xref: /php-src/ext/dom/xml_serializer.c (revision 1fdbb0ab)
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 "xml_serializer.h"
24 #include "namespace_compat.h"
25 #include "serialize_common.h"
26 #include "internal_helpers.h"
27 
28 // TODO: implement iterative approach instead of recursive?
29 
30 /* This file implements the XML serialization algorithm.
31  * https://w3c.github.io/DOM-Parsing/#dom-xmlserializer-serializetostring (Date 2021-05-02)
32  *
33  * The following are spec issues that were fixed in this implementation, but are not yet fixed
34  * in the spec itself:
35  * https://github.com/w3c/DOM-Parsing/issues/28
36  * https://github.com/w3c/DOM-Parsing/issues/29
37  * https://github.com/w3c/DOM-Parsing/issues/38
38  * https://github.com/w3c/DOM-Parsing/issues/43
39  * https://github.com/w3c/DOM-Parsing/issues/44
40  * https://github.com/w3c/DOM-Parsing/issues/45
41  * https://github.com/w3c/DOM-Parsing/issues/47
42  * https://github.com/w3c/DOM-Parsing/issues/50
43  * https://github.com/w3c/DOM-Parsing/issues/52
44  * https://github.com/w3c/DOM-Parsing/issues/59
45  * https://github.com/w3c/DOM-Parsing/issues/71
46  */
47 
48 #define TRY(x) do { if (UNEXPECTED((x) < 0)) { return -1; } } while (0)
49 #define TRY_OR_CLEANUP(x) do { if (UNEXPECTED((x) < 0)) { goto cleanup; } } while (0)
50 
51 #define xmlOutputBufferWriteLit(out, literal) xmlOutputBufferWrite((out), strlen("" literal), "" literal)
52 
53 /* https://w3c.github.io/DOM-Parsing/#dfn-namespace-prefix-map
54  * This associates a namespace uri with a list of possible prefixes. */
55 typedef struct {
56 	HashTable *ht;
57 } dom_xml_ns_prefix_map;
58 
59 /* https://w3c.github.io/DOM-Parsing/#dfn-local-prefixes-map */
60 typedef struct {
61 	HashTable ht;
62 } dom_xml_local_prefix_map;
63 
64 typedef struct {
65 	const xmlChar *prefix, *name;
66 } dom_qname_pair;
67 
68 static int dom_xml_serialization_algorithm(
69 	xmlSaveCtxtPtr ctxt,
70 	xmlOutputBufferPtr out,
71 	dom_xml_ns_prefix_map *namespace_prefix_map,
72 	xmlNodePtr node,
73 	const xmlChar *namespace,
74 	unsigned int *prefix_index,
75 	int indent
76 );
77 
dom_xml_str_equals_treat_nulls_as_empty(const xmlChar * s1,const xmlChar * s2)78 static bool dom_xml_str_equals_treat_nulls_as_empty(const xmlChar *s1, const xmlChar *s2)
79 {
80 	if (s1 == s2) {
81 		return true;
82 	}
83 	if (s1 == NULL) {
84 		return s2 == NULL || *s2 == '\0';
85 	}
86 	if (s2 == NULL) {
87 		/* Note: at this point we know that s1 != NULL. */
88 		return *s1 == '\0';
89 	}
90 	return strcmp((const char *) s1, (const char *) s2) == 0;
91 }
92 
dom_xml_str_equals_treat_nulls_as_nulls(const xmlChar * s1,const xmlChar * s2)93 static zend_always_inline bool dom_xml_str_equals_treat_nulls_as_nulls(const xmlChar *s1, const xmlChar *s2)
94 {
95 	if (s1 == s2) {
96 		return true;
97 	}
98 	if (s1 == NULL || s2 == NULL) {
99 		return false;
100 	}
101 	return strcmp((const char *) s1, (const char *) s2) == 0;
102 }
103 
dom_xml_ns_prefix_map_ctor(dom_xml_ns_prefix_map * map)104 static zend_always_inline void dom_xml_ns_prefix_map_ctor(dom_xml_ns_prefix_map *map)
105 {
106 	ALLOC_HASHTABLE(map->ht);
107 	zend_hash_init(map->ht, 8, NULL, NULL, false);
108 }
109 
dom_xml_ns_prefix_map_destroy(dom_xml_ns_prefix_map * map)110 static void dom_xml_ns_prefix_map_destroy(dom_xml_ns_prefix_map *map)
111 {
112 	HashTable *list;
113 	ZEND_HASH_MAP_FOREACH_PTR(map->ht, list) {
114 		if (GC_DELREF(list) == 0) {
115 			zval *tmp;
116 			ZEND_HASH_PACKED_FOREACH_VAL(list, tmp) {
117 				if (DOM_Z_IS_OWNED(tmp)) {
118 					efree(Z_PTR_P(tmp));
119 				}
120 			} ZEND_HASH_FOREACH_END();
121 
122 			zend_hash_destroy(list);
123 			efree(list);
124 		}
125 	} ZEND_HASH_FOREACH_END();
126 
127 	zend_hash_destroy(map->ht);
128 	efree(map->ht);
129 	map->ht = NULL;
130 }
131 
dom_xml_ns_prefix_map_dtor(dom_xml_ns_prefix_map * map)132 static zend_always_inline void dom_xml_ns_prefix_map_dtor(dom_xml_ns_prefix_map *map)
133 {
134 	if (GC_DELREF(map->ht) == 0) {
135 		dom_xml_ns_prefix_map_destroy(map);
136 	}
137 }
138 
dom_xml_ns_prefix_map_copy(dom_xml_ns_prefix_map * dst,const dom_xml_ns_prefix_map * src)139 static zend_always_inline void dom_xml_ns_prefix_map_copy(dom_xml_ns_prefix_map *dst, const dom_xml_ns_prefix_map *src)
140 {
141 	dst->ht = src->ht;
142 	GC_ADDREF(dst->ht);
143 }
144 
dom_xml_local_prefix_map_ctor(dom_xml_local_prefix_map * map)145 static zend_always_inline void dom_xml_local_prefix_map_ctor(dom_xml_local_prefix_map *map)
146 {
147 	zend_hash_init(&map->ht, 8, NULL, NULL, false);
148 }
149 
dom_xml_local_prefix_map_dtor(dom_xml_local_prefix_map * map)150 static zend_always_inline void dom_xml_local_prefix_map_dtor(dom_xml_local_prefix_map *map)
151 {
152 	zend_hash_destroy(&map->ht);
153 }
154 
dom_xml_local_prefix_map_add(dom_xml_local_prefix_map * map,const xmlChar * prefix,size_t prefix_len,const xmlChar * ns)155 static zend_always_inline void dom_xml_local_prefix_map_add(
156 	dom_xml_local_prefix_map *map,
157 	const xmlChar *prefix,
158 	size_t prefix_len,
159 	const xmlChar *ns
160 )
161 {
162 	ZEND_ASSERT(prefix != NULL);
163 	zend_hash_str_add_ptr(&map->ht, (const char *) prefix, prefix_len, (void *) ns);
164 }
165 
dom_xml_local_prefix_map_find(const dom_xml_local_prefix_map * map,const xmlChar * prefix,size_t prefix_len)166 static zend_always_inline const xmlChar *dom_xml_local_prefix_map_find(
167 	const dom_xml_local_prefix_map *map,
168 	const xmlChar *prefix,
169 	size_t prefix_len
170 )
171 {
172 	ZEND_ASSERT(prefix != NULL);
173 	return zend_hash_str_find_ptr(&map->ht, (const char *) prefix, prefix_len);
174 }
175 
dom_xml_local_prefix_map_conflicts(const dom_xml_local_prefix_map * map,const xmlChar * prefix,size_t prefix_len,const xmlChar * ns)176 static zend_always_inline bool dom_xml_local_prefix_map_conflicts(
177 	const dom_xml_local_prefix_map *map,
178 	const xmlChar *prefix,
179 	size_t prefix_len,
180 	const xmlChar *ns
181 )
182 {
183 	const xmlChar *result = dom_xml_local_prefix_map_find(map, prefix, prefix_len);
184 	if (result == NULL) {
185 		return false;
186 	}
187 	return !dom_xml_str_equals_treat_nulls_as_empty(result, ns);
188 }
189 
dom_xml_local_prefix_map_contains(const dom_xml_local_prefix_map * map,const xmlChar * prefix,size_t prefix_len)190 static zend_always_inline bool dom_xml_local_prefix_map_contains(
191 	const dom_xml_local_prefix_map *map,
192 	const xmlChar *prefix,
193 	size_t prefix_len
194 )
195 {
196 	return dom_xml_local_prefix_map_find(map, prefix, prefix_len) != NULL;
197 }
198 
199 /* https://w3c.github.io/DOM-Parsing/#dfn-add */
dom_xml_ns_prefix_map_add(dom_xml_ns_prefix_map * map,const xmlChar * prefix,bool prefix_owned,const xmlChar * ns,size_t ns_length)200 static void dom_xml_ns_prefix_map_add(
201 	dom_xml_ns_prefix_map *map,
202 	const xmlChar *prefix,
203 	bool prefix_owned,
204 	const xmlChar *ns,
205 	size_t ns_length
206 )
207 {
208 	ZEND_ASSERT(map->ht != NULL);
209 	ZEND_ASSERT(prefix != NULL);
210 
211 	if (ns == NULL) {
212 		ns = BAD_CAST "";
213 	}
214 
215 	if (GC_REFCOUNT(map->ht) > 1) {
216 		GC_DELREF(map->ht);
217 		map->ht = zend_array_dup(map->ht);
218 
219 		HashTable *list;
220 		ZEND_HASH_MAP_FOREACH_PTR(map->ht, list) {
221 			GC_ADDREF(list);
222 		} ZEND_HASH_FOREACH_END();
223 	}
224 
225 	/* 1. Let candidates list be the result of retrieving a list from map where there exists a key in map
226 	*     that matches the value of ns
227 	 *    or if there is no such key, then let candidates list be null. */
228 	HashTable *list = zend_hash_str_find_ptr(map->ht, (const char *) ns, ns_length);
229 
230 	/* 2. If candidates list is null, then create a new list with prefix as the only item in the list,
231 	 *    and associate that list with a new key ns in map. */
232 	if (list == NULL) {
233 		ALLOC_HASHTABLE(list);
234 		zend_hash_init(list, 8, NULL, NULL, false);
235 		zend_hash_str_add_new_ptr(map->ht, (const char *) ns, ns_length, list);
236 	} else if (GC_REFCOUNT(list) > 1) {
237 		GC_DELREF(list);
238 		list = zend_array_dup(list);
239 		zend_hash_str_update_ptr(map->ht, (const char *) ns, ns_length, list);
240 	}
241 
242 	/* 3. (Otherwise), append prefix to the end of candidates list. */
243 	zval tmp;
244 	if (prefix_owned) {
245 		DOM_Z_OWNED(&tmp, prefix);
246 	} else {
247 		DOM_Z_UNOWNED(&tmp, prefix);
248 	}
249 	zend_hash_next_index_insert_new(list, &tmp);
250 }
251 
252 /* https://w3c.github.io/DOM-Parsing/#dfn-found */
dom_get_candidates_list(dom_xml_ns_prefix_map * map,const xmlChar * ns,size_t ns_length)253 static zend_always_inline HashTable *dom_get_candidates_list(dom_xml_ns_prefix_map *map, const xmlChar *ns, size_t ns_length)
254 {
255 	ZEND_ASSERT(map->ht != NULL);
256 
257 	/* 1. Let candidates list be the result of retrieving a list from map where there exists a key in map that matches
258 	 *    the value of ns
259 	 *    or if there is no such key, then let candidates list be null. */
260 	return zend_hash_str_find_ptr(map->ht, (const char *) ns, ns_length);
261 }
262 
263 /* https://w3c.github.io/DOM-Parsing/#dfn-found */
dom_prefix_in_candidate_list(const HashTable * list,const xmlChar * prefix)264 static zend_always_inline bool dom_prefix_in_candidate_list(const HashTable *list, const xmlChar *prefix)
265 {
266 	ZEND_ASSERT(prefix != NULL);
267 
268 	if (list == NULL) {
269 		return false;
270 	}
271 
272 	/* 2. If the value of prefix occurs at least once in candidates list, return true, otherwise return false. */
273 	const char *tmp;
274 	ZEND_HASH_PACKED_FOREACH_PTR(list, tmp) {
275 		if (dom_xml_str_equals_treat_nulls_as_empty(BAD_CAST tmp, prefix)) {
276 			return true;
277 		}
278 	} ZEND_HASH_FOREACH_END();
279 
280 	return false;
281 }
282 
283 /* https://w3c.github.io/DOM-Parsing/#dfn-found */
dom_prefix_found_in_ns_prefix_map(dom_xml_ns_prefix_map * map,const xmlChar * prefix,const xmlChar * ns,size_t ns_length)284 static zend_always_inline bool dom_prefix_found_in_ns_prefix_map(
285 	dom_xml_ns_prefix_map *map,
286 	const xmlChar *prefix,
287 	const xmlChar *ns,
288 	size_t ns_length
289 )
290 {
291 	ZEND_ASSERT(ns != NULL);
292 	HashTable *list = dom_get_candidates_list(map, ns, ns_length);
293 	return dom_prefix_in_candidate_list(list, prefix);
294 }
295 
296 /* Helper to get the attribute value, will return "" instead of NULL for empty values, to mimic getAttribute()'s behaviour. */
dom_get_attribute_value(const xmlAttr * attr)297 static zend_always_inline const xmlChar *dom_get_attribute_value(const xmlAttr *attr)
298 {
299 	if (attr->children == NULL) {
300 		return BAD_CAST "";
301 	}
302 	return attr->children->content ? attr->children->content : BAD_CAST "";
303 }
304 
305 /* https://w3c.github.io/DOM-Parsing/#dfn-recording-the-namespace-information */
dom_recording_the_namespace_information(dom_xml_ns_prefix_map * namespace_prefix_map,dom_xml_local_prefix_map * local_prefixes_map,xmlNodePtr element)306 static const xmlChar *dom_recording_the_namespace_information(
307 	dom_xml_ns_prefix_map *namespace_prefix_map,
308 	dom_xml_local_prefix_map *local_prefixes_map,
309 	xmlNodePtr element
310 )
311 {
312 	ZEND_ASSERT(element->type == XML_ELEMENT_NODE);
313 
314 	/* 1. Let default namespace attr value be null. */
315 	const xmlChar *default_namespace_attr_value = NULL;
316 
317 	/* 2. [MAIN] For each attribute attr in element's attributes, in the order they are specified in the element's attribute list: */
318 	for (xmlAttrPtr attr = element->properties; attr != NULL; attr = attr->next) {
319 		/* Steps 2.1-2.2 fetch namespace information from the attribute, but let's defer that for simplicity to the if's body. */
320 
321 		/* 2.3. If the attribute namespace is the XMLNS namespace, then: */
322 		if (php_dom_ns_is_fast((xmlNodePtr) attr, php_dom_ns_is_xmlns_magic_token)) {
323 			/* 2.3.1. If attribute prefix is null, then attr is a default namespace declaration.
324 			 *        Set the default namespace attr value to attr's value and stop running these steps,
325 			 *        returning to Main to visit the next attribute. */
326 			if (attr->ns->prefix == NULL) {
327 				default_namespace_attr_value = dom_get_attribute_value(attr);
328 				continue;
329 			}
330 			/* 2.3.2. Otherwise, the attribute prefix is not null and attr is a namespace prefix definition.
331 			 *        Run the following steps: */
332 			else {
333 				/* 2.3.2.1. Let prefix definition be the value of attr's localName. */
334 				const xmlChar *prefix_definition = attr->name;
335 				ZEND_ASSERT(prefix_definition != NULL);
336 
337 				/* 2.3.2.2. Let namespace definition be the value of attr's value. */
338 				const xmlChar *namespace_definition = dom_get_attribute_value(attr);
339 				ZEND_ASSERT(namespace_definition != NULL);
340 
341 				/* 2.3.2.3. If namespace definition is the XML namespace, then stop running these steps,
342 				 *          and return to Main to visit the next attribute. */
343 				if (strcmp((const char *) namespace_definition, DOM_XML_NS_URI) == 0) {
344 					continue;
345 				}
346 
347 				/* 2.3.2.4. If namespace definition is the empty string (the declarative form of having no namespace),
348 				 *          then let namespace definition be null instead. */
349 				if (*namespace_definition == '\0') {
350 					namespace_definition = NULL;
351 				}
352 
353 				size_t namespace_definition_length = namespace_definition == NULL ? 0 : strlen((const char *) namespace_definition);
354 
355 				/* 2.3.2.5. If prefix definition is found in map given the namespace namespace definition,
356 				 *          then stop running these steps, and return to Main to visit the next attribute. */
357 				if (dom_prefix_found_in_ns_prefix_map(namespace_prefix_map, prefix_definition, namespace_definition, namespace_definition_length)) {
358 					continue;
359 				}
360 
361 				/* 2.3.2.6. Add the prefix prefix definition to map given namespace namespace definition. */
362 				dom_xml_ns_prefix_map_add(namespace_prefix_map, prefix_definition, false, namespace_definition, namespace_definition_length);
363 
364 				/* 2.3.2.7. Add the value of prefix definition as a new key to the local prefixes map,
365 				 *          with the namespace definition as the key's value replacing the value of null with the empty string if applicable. */
366 				size_t prefix_definition_length = strlen((const char *) prefix_definition);
367 				namespace_definition = namespace_definition == NULL ? BAD_CAST "" : namespace_definition;
368 				dom_xml_local_prefix_map_add(local_prefixes_map, prefix_definition, prefix_definition_length, namespace_definition);
369 			}
370 		}
371 	}
372 
373 	/* 3. Return the value of default namespace attr value. */
374 	return default_namespace_attr_value;
375 }
376 
377 /* https://w3c.github.io/DOM-Parsing/#dfn-retrieving-a-preferred-prefix-string */
dom_retrieve_a_preferred_prefix_string(dom_xml_ns_prefix_map * namespace_prefix_map,dom_xml_local_prefix_map * local_prefixes_map,const xmlChar * preferred_prefix,const xmlChar * ns,size_t ns_length)378 static const xmlChar *dom_retrieve_a_preferred_prefix_string(
379 	dom_xml_ns_prefix_map *namespace_prefix_map,
380 	dom_xml_local_prefix_map *local_prefixes_map,
381 	const xmlChar *preferred_prefix,
382 	const xmlChar *ns,
383 	size_t ns_length
384 )
385 {
386 	ZEND_ASSERT(namespace_prefix_map->ht != NULL);
387 
388 	if (ns == NULL) {
389 		ns = BAD_CAST "";
390 	}
391 
392 	/* 1. Let candidates list be the result of retrieving a list from map where there exists a key in map that matches
393 	 *    the value of ns or if there is no such key, then stop running these steps, and return the null value. */
394 	HashTable *list = dom_get_candidates_list(namespace_prefix_map, ns, ns_length);
395 	if (list == NULL) {
396 		return NULL;
397 	}
398 
399 	/* 2. Otherwise, for each prefix value prefix in candidates list, iterating from beginning to end: */
400 	const xmlChar *prefix = NULL;
401 	const xmlChar *last_non_conflicting_in_list = NULL;
402 
403 	/* Reverse so that the "nearest" ns gets priority: https://github.com/w3c/DOM-Parsing/issues/45 */
404 	ZEND_HASH_PACKED_REVERSE_FOREACH_PTR(list, prefix) {
405 		ZEND_ASSERT(prefix != NULL);
406 
407 		/* 2.1. If prefix matches preferred prefix, then stop running these steps and return prefix. */
408 		/* Adapted for https://github.com/w3c/DOM-Parsing/issues/45 */
409 		if (!dom_xml_local_prefix_map_conflicts(local_prefixes_map, prefix, strlen((const char *) prefix), ns)) {
410 			if (dom_xml_str_equals_treat_nulls_as_empty(preferred_prefix, prefix)) {
411 				return prefix;
412 			}
413 
414 			if (last_non_conflicting_in_list == NULL) {
415 				last_non_conflicting_in_list = prefix;
416 			}
417 		}
418 	} ZEND_HASH_FOREACH_END();
419 
420 	/* 2.2. If prefix is the last item in the candidates list, then stop running these steps and return prefix. */
421 	/* Note: previously the last item was "prefix", but we loop backwards now. */
422 	return last_non_conflicting_in_list;
423 }
424 
425 /* https://w3c.github.io/DOM-Parsing/#dfn-generating-a-prefix */
dom_xml_generate_a_prefix(dom_xml_ns_prefix_map * map,dom_xml_local_prefix_map * local_prefixes_map,const xmlChar * new_namespace,size_t new_namespace_length,unsigned int * prefix_index)426 static xmlChar *dom_xml_generate_a_prefix(
427 	dom_xml_ns_prefix_map *map,
428 	dom_xml_local_prefix_map *local_prefixes_map,
429 	const xmlChar *new_namespace,
430 	size_t new_namespace_length,
431 	unsigned int *prefix_index
432 )
433 {
434 	/* 1. Let generated prefix be the concatenation of the string "ns" and the current numerical value of prefix index. */
435 	char buffer[32];
436 	buffer[0] = 'n';
437 	buffer[1] = 's';
438 	size_t length;
439 	do {
440 		length = snprintf(buffer + 2, sizeof(buffer) - 2, "%u", *prefix_index) + 2;
441 
442 		/* 2. Let the value of prefix index be incremented by one. */
443 		(*prefix_index)++;
444 
445 		/* Loop condition is for https://github.com/w3c/DOM-Parsing/issues/44 */
446 	} while (dom_xml_local_prefix_map_contains(local_prefixes_map, (const xmlChar *) buffer, length));
447 
448 	xmlChar *generated_prefix = emalloc(length + 1);
449 	memcpy(generated_prefix, buffer, length + 1);
450 
451 	/* 3. Add to map the generated prefix given the new namespace namespace. */
452 	dom_xml_ns_prefix_map_add(map, generated_prefix, true, new_namespace, new_namespace_length);
453 	/* Continuation of https://github.com/w3c/DOM-Parsing/issues/44 */
454 	dom_xml_local_prefix_map_add(local_prefixes_map, generated_prefix, length, new_namespace);
455 
456 	/* 4. Return the value of generated prefix. */
457 	return generated_prefix;
458 }
459 
dom_xml_output_qname(xmlOutputBufferPtr out,const dom_qname_pair * qname)460 static int dom_xml_output_qname(xmlOutputBufferPtr out, const dom_qname_pair *qname)
461 {
462 	if (qname->prefix != NULL) {
463 		TRY(xmlOutputBufferWriteString(out, (const char *) qname->prefix));
464 		TRY(xmlOutputBufferWriteLit(out, ":"));
465 	}
466 	return xmlOutputBufferWriteString(out, (const char *) qname->name);
467 }
468 
469 /* This is a utility method used by both
470  * https://w3c.github.io/DOM-Parsing/#dfn-xml-serializing-an-element-node
471  * and https://w3c.github.io/DOM-Parsing/#dfn-serializing-an-attribute-value */
dom_xml_common_text_serialization(xmlOutputBufferPtr out,const char * content,bool attribute_mode)472 static int dom_xml_common_text_serialization(xmlOutputBufferPtr out, const char *content, bool attribute_mode)
473 {
474 	if (content == NULL) {
475 		return 0;
476 	}
477 
478 	const char *last_output = content;
479 	const char *mask = attribute_mode ? "&<>\"\t\n\r" : "&<>";
480 
481 	while (true) {
482 		size_t chunk_length = strcspn(content, mask);
483 
484 		content += chunk_length;
485 		if (*content == '\0') {
486 			break;
487 		}
488 
489 		TRY(xmlOutputBufferWrite(out, content - last_output, last_output));
490 
491 		switch (*content) {
492 			case '&': {
493 				TRY(xmlOutputBufferWriteLit(out, "&amp;"));
494 				break;
495 			}
496 
497 			case '<': {
498 				TRY(xmlOutputBufferWriteLit(out, "&lt;"));
499 				break;
500 			}
501 
502 			case '>': {
503 				TRY(xmlOutputBufferWriteLit(out, "&gt;"));
504 				break;
505 			}
506 
507 			case '"': {
508 				TRY(xmlOutputBufferWriteLit(out, "&quot;"));
509 				break;
510 			}
511 
512 			/* The following three are added to address https://github.com/w3c/DOM-Parsing/issues/59 */
513 
514 			case '\t': {
515 				TRY(xmlOutputBufferWriteLit(out, "&#9;"));
516 				break;
517 			}
518 
519 			case '\n': {
520 				TRY(xmlOutputBufferWriteLit(out, "&#10;"));
521 				break;
522 			}
523 
524 			case '\r': {
525 				TRY(xmlOutputBufferWriteLit(out, "&#13;"));
526 				break;
527 			}
528 		}
529 
530 		content++;
531 		last_output = content;
532 	}
533 
534 	return xmlOutputBufferWrite(out, content - last_output, last_output);
535 }
536 
537 /* https://w3c.github.io/DOM-Parsing/#dfn-xml-serializing-an-element-node */
dom_xml_serialize_text_node(xmlOutputBufferPtr out,xmlNodePtr text)538 static zend_always_inline int dom_xml_serialize_text_node(xmlOutputBufferPtr out, xmlNodePtr text)
539 {
540 	/* 1. If the require well-formed flag is set ...
541 	 *    => N/A */
542 
543 	return dom_xml_common_text_serialization(out, (const char *) text->content, false);
544 }
545 
dom_xml_serialize_attribute_node_value(xmlOutputBufferPtr out,xmlAttrPtr attr)546 static int dom_xml_serialize_attribute_node_value(xmlOutputBufferPtr out, xmlAttrPtr attr)
547 {
548 	TRY(xmlOutputBufferWriteString(out, (const char *) attr->name));
549 	TRY(xmlOutputBufferWriteLit(out, "=\""));
550 	for (xmlNodePtr child = attr->children; child != NULL; child = child->next) {
551 		if (child->type == XML_TEXT_NODE) {
552 			if (child->content != NULL) {
553 				TRY(dom_xml_common_text_serialization(out, (const char *) child->content, true));
554 			}
555 		} else if (child->type == XML_ENTITY_REF_NODE) {
556 			TRY(xmlOutputBufferWriteLit(out, "&"));
557 			TRY(dom_xml_common_text_serialization(out, (const char *) child->name, true));
558 			TRY(xmlOutputBufferWriteLit(out, ";"));
559 		}
560 	}
561 	return xmlOutputBufferWriteLit(out, "\"");
562 }
563 
564 /* Spec says to do nothing, but that's inconsistent/wrong, see https://github.com/w3c/DOM-Parsing/issues/28 */
dom_xml_serialize_attribute_node(xmlOutputBufferPtr out,xmlNodePtr attr)565 static int dom_xml_serialize_attribute_node(xmlOutputBufferPtr out, xmlNodePtr attr)
566 {
567 	if (attr->ns != NULL && attr->ns->prefix != NULL) {
568 		TRY(xmlOutputBufferWriteString(out, (const char *) attr->ns->prefix));
569 		TRY(xmlOutputBufferWriteLit(out, ":"));
570 	}
571 	return dom_xml_serialize_attribute_node_value(out, (xmlAttrPtr) attr);
572 }
573 
574 /* https://w3c.github.io/DOM-Parsing/#dfn-xml-serializing-a-comment-node */
dom_xml_serialize_comment_node(xmlOutputBufferPtr out,xmlNodePtr comment)575 static int dom_xml_serialize_comment_node(xmlOutputBufferPtr out, xmlNodePtr comment)
576 {
577 	/* 1. If the require well-formed flag is set ...
578 	 *    => N/A */
579 
580 	TRY(xmlOutputBufferWriteLit(out, "<!--"));
581 	if (EXPECTED(comment->content != NULL)) {
582 		TRY(xmlOutputBufferWriteString(out, (const char *) comment->content));
583 	}
584 	return xmlOutputBufferWriteLit(out, "-->");
585 }
586 
587 /* https://w3c.github.io/DOM-Parsing/#xml-serializing-a-processinginstruction-node */
dom_xml_serialize_processing_instruction(xmlOutputBufferPtr out,xmlNodePtr pi)588 static int dom_xml_serialize_processing_instruction(xmlOutputBufferPtr out, xmlNodePtr pi)
589 {
590 	/* Steps 1-2 deal with well-formed flag
591 	 *    => N/A */
592 
593 	TRY(xmlOutputBufferWriteLit(out, "<?"));
594 	TRY(xmlOutputBufferWriteString(out, (const char *) pi->name));
595 	TRY(xmlOutputBufferWriteLit(out, " "));
596 	if (EXPECTED(pi->content != NULL)) {
597 		TRY(xmlOutputBufferWriteString(out, (const char *) pi->content));
598 	}
599 	return xmlOutputBufferWriteLit(out, "?>");
600 }
601 
602 /* https://github.com/w3c/DOM-Parsing/issues/38
603  * and https://github.com/w3c/DOM-Parsing/blob/ab8d1ac9699ed43ae6de9f4be2b0f3cfc5f3709e/index.html#L1510 */
dom_xml_serialize_cdata_section_node(xmlOutputBufferPtr out,xmlNodePtr cdata)604 static int dom_xml_serialize_cdata_section_node(xmlOutputBufferPtr out, xmlNodePtr cdata)
605 {
606 	TRY(xmlOutputBufferWriteLit(out, "<![CDATA["));
607 	if (EXPECTED(cdata->content != NULL)) {
608 		TRY(xmlOutputBufferWriteString(out, (const char *) cdata->content));
609 	}
610 	return xmlOutputBufferWriteLit(out, "]]>");
611 }
612 
613 /* https://w3c.github.io/DOM-Parsing/#dfn-xml-serialization-of-the-attributes */
dom_xml_serialize_attributes(xmlOutputBufferPtr out,xmlNodePtr element,dom_xml_ns_prefix_map * map,dom_xml_local_prefix_map * local_prefixes_map,unsigned int * prefix_index,bool ignore_namespace_definition_attribute)614 static int dom_xml_serialize_attributes(
615 	xmlOutputBufferPtr out,
616 	xmlNodePtr element,
617 	dom_xml_ns_prefix_map *map,
618 	dom_xml_local_prefix_map *local_prefixes_map,
619 	unsigned int *prefix_index,
620 	bool ignore_namespace_definition_attribute
621 )
622 {
623 	/* 1. Let result be the empty string.
624 	 *    => We're going to write directly to the output buffer. */
625 
626 	/* 2. Let localname set be a new empty namespace localname set.
627 	 *    => N/A this is only required for well-formedness */
628 
629 	/* 3. [LOOP] For each attribute attr in element's attributes, in the order they are specified in the element's attribute list: */
630 	for (xmlAttrPtr attr = element->properties; attr != NULL; attr = attr->next) {
631 		/* 3.1. If the require well-formed flag is set ...
632 		 *      => N/A */
633 
634 		/* 3.2. Create a new tuple consisting of attr's namespaceURI attribute and localName attribute, and add it to the localname set.
635 		 *      => N/A this is only required for well-formedness */
636 
637 		/* 3.3. Let attribute namespace be the value of attr's namespaceURI value. */
638 		const xmlChar *attribute_namespace = attr->ns == NULL ? NULL : attr->ns->href;
639 
640 		/* 3.4. Let candidate prefix be null. */
641 		const xmlChar *candidate_prefix = NULL;
642 
643 		/* 3.5. If attribute namespace is not null, then run these sub-steps: */
644 		if (attribute_namespace != NULL) {
645 			/* 3.5.1. Let candidate prefix be the result of retrieving a preferred prefix string from map
646 			 *        given namespace attribute namespace with preferred prefix being attr's prefix value. */
647 			candidate_prefix = dom_retrieve_a_preferred_prefix_string(
648 				map,
649 				local_prefixes_map,
650 				attr->ns->prefix,
651 				attribute_namespace,
652 				strlen((const char *) attribute_namespace)
653 			);
654 
655 			/* 3.5.2. If the value of attribute namespace is the XMLNS namespace, then run these steps: */
656 			if (php_dom_ns_is_fast((xmlNodePtr) attr, php_dom_ns_is_xmlns_magic_token)) {
657 				const xmlChar *attr_value = dom_get_attribute_value(attr);
658 
659 				/* 3.5.2.1. If any of the following are true, then stop running these steps and goto Loop to visit the next attribute: */
660 				/* the attr's value is the XML namespace; */
661 				if (strcmp((const char *) attr_value, DOM_XML_NS_URI) == 0) {
662 					continue;
663 				}
664 				/* the attr's prefix is null and the ignore namespace definition attribute flag is true */
665 				if (ignore_namespace_definition_attribute && attr->ns->prefix == NULL) {
666 					/* https://github.com/w3c/DOM-Parsing/issues/47 */
667 					if (!dom_xml_str_equals_treat_nulls_as_empty(element->ns == NULL ? NULL : element->ns->href, attr_value)) {
668 						continue;
669 					}
670 				}
671 				/* the attr's prefix is not null and either */
672 				if (attr->ns->prefix != NULL) {
673 					/* the attr's localName is not a key contained in the local prefixes map
674 					 * or the attr's localName is present in the local prefixes map but the value of the key does not match attr's value
675 					 * and furthermore that the attr's localName (as the prefix to find) is found in the namespace prefix map
676 					 * given the namespace consisting of the attr's value */
677 					const xmlChar *value = dom_xml_local_prefix_map_find(local_prefixes_map, attr->name, strlen((const char *) attr->name));
678 					if (value == NULL || strcmp((const char *) value, (const char *) attr_value) != 0) {
679 						if (dom_prefix_found_in_ns_prefix_map(map, attr->name, attr_value, strlen((const char *) attr_value))) {
680 							continue;
681 						}
682 					}
683 				}
684 
685 				/* 3.5.2.2. If the require well-formed flag is set ...
686 		 		 *      => N/A */
687 				/* 3.5.2.3. If the require well-formed flag is set ...
688 		 		 *      => N/A */
689 
690 				/* 3.5.2.4. the attr's prefix matches the string "xmlns", then let candidate prefix be the string "xmlns". */
691 				if (attr->ns->prefix != NULL && strcmp((const char *) attr->ns->prefix, "xmlns") == 0) {
692 					candidate_prefix = BAD_CAST "xmlns";
693 				}
694 			}
695 			/* 3.5.3. Otherwise, the attribute namespace in not the XMLNS namespace. Run these steps: */
696 			else if (candidate_prefix == NULL) { /* https://github.com/w3c/DOM-Parsing/issues/29 */
697 				/* Continuation of https://github.com/w3c/DOM-Parsing/issues/29 */
698 				if (attr->ns->prefix == NULL
699 					|| dom_xml_local_prefix_map_contains(local_prefixes_map, attr->ns->prefix, strlen((const char *) attr->ns->prefix))) {
700 					/* 3.5.3.1. Let candidate prefix be the result of generating a prefix providing map,
701 					 *          attribute namespace, and prefix index as input. */
702 					candidate_prefix = dom_xml_generate_a_prefix(
703 						map,
704 						local_prefixes_map,
705 						attribute_namespace,
706 						strlen((const char *) attribute_namespace),
707 						prefix_index
708 					);
709 				} else {
710 					candidate_prefix = attr->ns->prefix;
711 					/* Continuation of https://github.com/w3c/DOM-Parsing/issues/29 */
712 					dom_xml_ns_prefix_map_add(
713 						map,
714 						candidate_prefix,
715 						false,
716 						attribute_namespace,
717 						strlen((const char *) attribute_namespace)
718 					);
719 					dom_xml_local_prefix_map_add(
720 						local_prefixes_map,
721 						candidate_prefix,
722 						strlen((const char *) candidate_prefix),
723 						attribute_namespace
724 					);
725 				}
726 
727 				/* 3.5.3.2. Append the following to result, in the order listed: */
728 				TRY(xmlOutputBufferWriteLit(out, " xmlns:"));
729 				TRY(xmlOutputBufferWriteString(out, (const char *) candidate_prefix));
730 				TRY(xmlOutputBufferWriteLit(out, "=\""));
731 				TRY(dom_xml_common_text_serialization(out, (const char *) attribute_namespace, true));
732 				TRY(xmlOutputBufferWriteLit(out, "\""));
733 			}
734 		}
735 
736 		/* 3.6. Append a " " (U+0020 SPACE) to result. */
737 		TRY(xmlOutputBufferWriteLit(out, " "));
738 
739 		/* 3.7. If candidate prefix is not null, then append to result the concatenation of candidate prefix with ":" (U+003A COLON). */
740 		if (candidate_prefix != NULL) {
741 			TRY(xmlOutputBufferWriteString(out, (const char *) candidate_prefix));
742 			TRY(xmlOutputBufferWriteLit(out, ":"));
743 		}
744 
745 		/* 3.8. If the require well-formed flag is set ...
746 		 *      => N/A */
747 
748 		/* 3.9. Append the following strings to result, in the order listed: */
749 		dom_xml_serialize_attribute_node_value(out, attr);
750 	}
751 
752 	/* 4. Return the value of result.
753 	 *    => We're writing directly to the output buffer. */
754 	return 0;
755 }
756 
757 /* Only format output if there are no text/entityrefs/cdata nodes as children. */
dom_xml_should_format_element(xmlNodePtr element)758 static bool dom_xml_should_format_element(xmlNodePtr element)
759 {
760 	xmlNodePtr child = element->children;
761 	ZEND_ASSERT(child != NULL);
762 	do {
763 		if (child->type == XML_TEXT_NODE || child->type == XML_ENTITY_REF_NODE || child->type == XML_CDATA_SECTION_NODE) {
764 			return false;
765 		}
766 		child = child->next;
767 	} while (child != NULL);
768 	return true;
769 }
770 
dom_xml_output_indents(xmlOutputBufferPtr out,int indent)771 static int dom_xml_output_indents(xmlOutputBufferPtr out, int indent)
772 {
773 	TRY(xmlOutputBufferWriteLit(out, "\n"));
774 	for (int i = 0; i < indent; i++) {
775 		TRY(xmlOutputBufferWriteLit(out, "  "));
776 	}
777 	return 0;
778 }
779 
780 /* https://w3c.github.io/DOM-Parsing/#dfn-xml-serializing-an-element-node */
dom_xml_serialize_element_node(xmlSaveCtxtPtr ctxt,xmlOutputBufferPtr out,const xmlChar * namespace,dom_xml_ns_prefix_map * namespace_prefix_map,xmlNodePtr element,unsigned int * prefix_index,int indent)781 static int dom_xml_serialize_element_node(
782 	xmlSaveCtxtPtr ctxt,
783 	xmlOutputBufferPtr out,
784 	const xmlChar *namespace,
785 	dom_xml_ns_prefix_map *namespace_prefix_map,
786 	xmlNodePtr element,
787 	unsigned int *prefix_index,
788 	int indent
789 )
790 {
791 	bool should_format = indent >= 0 && element->children != NULL && dom_xml_should_format_element(element);
792 
793 	/* 1. If the require well-formed flag is set ...
794 	 *    => N/A */
795 
796 	/* 2. Let markup be the string "<" (U+003C LESS-THAN SIGN). */
797 	TRY(xmlOutputBufferWriteLit(out, "<"));
798 
799 	/* 3. Let qualified name be an empty string.
800 	 *    => We're going to do it a bit differently.
801 	 *       To avoid string allocations, we're going to store the qualified name separately as prefix+name.
802 	 *       If the prefix is NULL then the qualified name will be == name, otherwise == prefix:name. */
803 	dom_qname_pair qualified_name = { NULL, NULL };
804 
805 	/* 4. Let skip end tag be a boolean flag with value false. */
806 	bool skip_end_tag = false;
807 
808 	/* 5. Let ignore namespace definition attribute be a boolean flag with value false. */
809 	bool ignore_namespace_definition_attribute = false;
810 
811 	/* 6. Given prefix map, copy a namespace prefix map and let map be the result. */
812 	dom_xml_ns_prefix_map map;
813 	dom_xml_ns_prefix_map_copy(&map, namespace_prefix_map);
814 
815 	/* 7. Let local prefixes map be an empty map. */
816 	dom_xml_local_prefix_map local_prefixes_map;
817 	dom_xml_local_prefix_map_ctor(&local_prefixes_map);
818 
819 	/* 8. Let local default namespace be the result of recording the namespace information for node given map and local prefixes map. */
820 	const xmlChar *local_default_namespace = dom_recording_the_namespace_information(&map, &local_prefixes_map, element);
821 
822 	/* 9. Let inherited ns be a copy of namespace. */
823 	const xmlChar *inherited_ns = namespace;
824 
825 	/* 10. Let ns be the value of node's namespaceURI attribute. */
826 	const xmlChar *const ns = element->ns == NULL ? NULL : element->ns->href;
827 
828 	/* 11. If inherited ns is equal to ns, then: */
829 	if (dom_xml_str_equals_treat_nulls_as_nulls(inherited_ns, ns)) {
830 		/* 11.1. If local default namespace is not null, then set ignore namespace definition attribute to true. */
831 		if (local_default_namespace != NULL) {
832 			ignore_namespace_definition_attribute = true;
833 		}
834 
835 		/* 11.2. If ns is the XML namespace,
836 		 *       then append to qualified name the concatenation of the string "xml:" and the value of node's localName. */
837 		if (php_dom_ns_is_fast(element, php_dom_ns_is_xml_magic_token)) {
838 			qualified_name.prefix = BAD_CAST "xml";
839 			qualified_name.name = element->name;
840 		}
841 		/* 11.3. Otherwise, append to qualified name the value of node's localName. */
842 		else {
843 			qualified_name.name = element->name;
844 		}
845 
846 		/* 11.4. Append the value of qualified name to markup. */
847 		TRY_OR_CLEANUP(dom_xml_output_qname(out, &qualified_name));
848 	}
849 	/* 12. Otherwise, inherited ns is not equal to ns */
850 	else {
851 		/* 12.1. Let prefix be the value of node's prefix attribute. */
852 		const xmlChar *prefix = element->ns == NULL ? NULL : element->ns->prefix;
853 
854 		/* 12.2. Let candidate prefix be the result of retrieving a preferred prefix string prefix from map given namespace ns. */
855 		/* https://github.com/w3c/DOM-Parsing/issues/52 */
856 		const xmlChar *candidate_prefix;
857 		if (prefix == NULL && dom_xml_str_equals_treat_nulls_as_empty(ns, local_default_namespace)) {
858 			candidate_prefix = NULL;
859 		} else {
860 			size_t ns_length = ns == NULL ? 0 : strlen((const char *) ns);
861 			candidate_prefix = dom_retrieve_a_preferred_prefix_string(&map, &local_prefixes_map, prefix, ns, ns_length);
862 		}
863 
864 		/* 12.3. If the value of prefix matches "xmlns", then run the following steps: */
865 		if (prefix != NULL && strcmp((const char *) prefix, "xmlns") == 0) {
866 			/* Step 1 deals with well-formedness, which we don't implement here. */
867 
868 			/* 12.3.2. Let candidate prefix be the value of prefix. */
869 			candidate_prefix = prefix;
870 		}
871 
872 		/* 12.4. if candidate prefix is not null (a namespace prefix is defined which maps to ns), then: */
873 		if (candidate_prefix != NULL) {
874 			/* 12.4.1. Append to qualified name the concatenation of candidate prefix, ":" (U+003A COLON), and node's localName. */
875 			qualified_name.prefix = candidate_prefix;
876 			qualified_name.name = element->name;
877 
878 			/* 12.4.2. If the local default namespace is not null (there exists a locally-defined default namespace declaration attribute)
879 			 *         and its value is not the XML namespace ... */
880 			if (local_default_namespace != NULL && strcmp((const char *) local_default_namespace, DOM_XML_NS_URI) != 0) {
881 				if (*local_default_namespace == '\0') {
882 					inherited_ns = NULL;
883 				} else {
884 					inherited_ns = local_default_namespace;
885 				}
886 			}
887 
888 			/* 12.4.3. Append the value of qualified name to markup. */
889 			TRY_OR_CLEANUP(dom_xml_output_qname(out, &qualified_name));
890 		}
891 		/* 12.5. Otherwise, if prefix is not null, then: */
892 		else if (prefix != NULL) {
893 			size_t ns_length = ns == NULL ? 0 : strlen((const char *) ns);
894 
895 			/* 12.5.1. If the local prefixes map contains a key matching prefix, ... */
896 			size_t prefix_length = strlen((const char *) prefix);
897 			if (dom_xml_local_prefix_map_contains(&local_prefixes_map, prefix, prefix_length)) {
898 				prefix = dom_xml_generate_a_prefix(&map, &local_prefixes_map, ns, ns_length, prefix_index);
899 			} else { /* else branch fixes spec issue: generating a prefix already adds it to the maps. */
900 				/* 12.5.2. Add prefix to map given namespace ns. */
901 				dom_xml_ns_prefix_map_add(&map, prefix, false, ns, ns_length);
902 				/* This is not spelled out in spec, but we have to do this to avoid conflicts (see default_namespace_move.phpt). */
903 				dom_xml_local_prefix_map_add(&local_prefixes_map, prefix, prefix_length, ns);
904 			}
905 
906 			/* 12.5.3. Append to qualified name the concatenation of prefix, ":" (U+003A COLON), and node's localName. */
907 			qualified_name.prefix = prefix;
908 			qualified_name.name = element->name;
909 
910 			/* 12.5.4. Append the value of qualified name to markup. */
911 			TRY_OR_CLEANUP(dom_xml_output_qname(out, &qualified_name));
912 
913 			/* 12.5.5. Append the following to markup, in the order listed: ... */
914 			TRY_OR_CLEANUP(xmlOutputBufferWriteLit(out, " xmlns:")); /* 12.5.5.1 - 12.5.5.2 */
915 			TRY_OR_CLEANUP(xmlOutputBufferWriteString(out, (const char *) prefix));
916 			TRY_OR_CLEANUP(xmlOutputBufferWriteLit(out, "=\""));
917 			TRY_OR_CLEANUP(dom_xml_common_text_serialization(out, (const char *) ns, true));
918 			TRY_OR_CLEANUP(xmlOutputBufferWriteLit(out, "\""));
919 
920 			/* 12.5.6. If local default namespace is not null ... (editorial numbering error: https://github.com/w3c/DOM-Parsing/issues/43) */
921 			if (local_default_namespace != NULL) {
922 				if (*local_default_namespace == '\0') {
923 					inherited_ns = NULL;
924 				} else {
925 					inherited_ns = local_default_namespace;
926 				}
927 			}
928 		}
929 		/* 12.6. Otherwise, if local default namespace is null, or local default namespace is not null and its value is not equal to ns, then: */
930 		/* Note: https://github.com/w3c/DOM-Parsing/issues/47 */
931 		else if (local_default_namespace == NULL || !dom_xml_str_equals_treat_nulls_as_empty(local_default_namespace, ns)) {
932 			/* 12.6.1. Set the ignore namespace definition attribute flag to true. */
933 			ignore_namespace_definition_attribute = true;
934 
935 			/* 12.6.2. Append to qualified name the value of node's localName. */
936 			qualified_name.name = element->name;
937 
938 			/* 12.6.3. Let the value of inherited ns be ns. */
939 			inherited_ns = ns;
940 
941 			/* 12.6.4. Append the value of qualified name to markup. */
942 			TRY_OR_CLEANUP(dom_xml_output_qname(out, &qualified_name));
943 
944 			/* 12.6.5. Append the following to markup, in the order listed: ... */
945 			TRY_OR_CLEANUP(xmlOutputBufferWriteLit(out, " xmlns=\"")); /* 12.6.5.1 - 12.6.5.2 */
946 			TRY_OR_CLEANUP(dom_xml_common_text_serialization(out, (const char *) ns, true));
947 			TRY_OR_CLEANUP(xmlOutputBufferWriteLit(out, "\""));
948 		}
949 		/* 12.7. Otherwise, the node has a local default namespace that matches ns ... */
950 		else {
951 			qualified_name.name = element->name;
952 			inherited_ns = ns;
953 			TRY_OR_CLEANUP(dom_xml_output_qname(out, &qualified_name));
954 		}
955 	}
956 
957 	/* 13. Append to markup the result of the XML serialization of node's attributes given map, prefix index,
958 	 *     local prefixes map, ignore namespace definition attribute flag, and require well-formed flag. */
959 	TRY_OR_CLEANUP(dom_xml_serialize_attributes(out, element, &map, &local_prefixes_map, prefix_index, ignore_namespace_definition_attribute));
960 
961 	/* 14. If ns is the HTML namespace, and the node's list of children is empty, and the node's localName matches
962 	 *     any one of the following void elements: ... */
963 	if (element->children == NULL) {
964 		if (xmlSaveNoEmptyTags) {
965 			/* Do nothing, use the <x></x> closing style. */
966 		} else if (php_dom_ns_is_fast(element, php_dom_ns_is_html_magic_token)) {
967 			size_t name_length = strlen((const char *) element->name);
968 			if (dom_local_name_compare_ex(element, "area", strlen("area"), name_length)
969 				|| dom_local_name_compare_ex(element, "base", strlen("base"), name_length)
970 				|| dom_local_name_compare_ex(element, "basefont", strlen("basefont"), name_length)
971 				|| dom_local_name_compare_ex(element, "bgsound", strlen("bgsound"), name_length)
972 				|| dom_local_name_compare_ex(element, "br", strlen("br"), name_length)
973 				|| dom_local_name_compare_ex(element, "col", strlen("col"), name_length)
974 				|| dom_local_name_compare_ex(element, "embed", strlen("embed"), name_length)
975 				|| dom_local_name_compare_ex(element, "frame", strlen("frame"), name_length)
976 				|| dom_local_name_compare_ex(element, "hr", strlen("hr"), name_length)
977 				|| dom_local_name_compare_ex(element, "img", strlen("img"), name_length)
978 				|| dom_local_name_compare_ex(element, "input", strlen("input"), name_length)
979 				|| dom_local_name_compare_ex(element, "keygen", strlen("keygen"), name_length)
980 				|| dom_local_name_compare_ex(element, "link", strlen("link"), name_length)
981 				|| dom_local_name_compare_ex(element, "menuitem", strlen("menuitem"), name_length)
982 				|| dom_local_name_compare_ex(element, "meta", strlen("meta"), name_length)
983 				|| dom_local_name_compare_ex(element, "param", strlen("param"), name_length)
984 				|| dom_local_name_compare_ex(element, "source", strlen("source"), name_length)
985 				|| dom_local_name_compare_ex(element, "track", strlen("track"), name_length)
986 				|| dom_local_name_compare_ex(element, "wbr", strlen("wbr"), name_length)) {
987 				TRY_OR_CLEANUP(xmlOutputBufferWriteLit(out, " /"));
988 				skip_end_tag = true;
989 			}
990 		} else {
991 			/* 15. If ns is not the HTML namespace, and the node's list of children is empty,
992 			 *     then append "/" (U+002F SOLIDUS) to markup and set the skip end tag flag to true. */
993 			TRY_OR_CLEANUP(xmlOutputBufferWriteLit(out, "/"));
994 			skip_end_tag = true;
995 		}
996 	}
997 
998 	/* 16. Append ">" (U+003E GREATER-THAN SIGN) to markup. */
999 	TRY_OR_CLEANUP(xmlOutputBufferWriteLit(out, ">"));
1000 
1001 	/* 17. If the value of skip end tag is true, then return the value of markup and skip the remaining steps. */
1002 	if (!skip_end_tag) {
1003 		/* Step 18 deals with template elements which we don't support. */
1004 
1005 		if (should_format) {
1006 			indent++;
1007 		} else {
1008 			indent = -1;
1009 		}
1010 
1011 		/* 19. Otherwise, append to markup the result of running the XML serialization algorithm on each of node's children. */
1012 		for (xmlNodePtr child = element->children; child != NULL; child = child->next) {
1013 			if (should_format) {
1014 				TRY_OR_CLEANUP(dom_xml_output_indents(out, indent));
1015 			}
1016 			TRY_OR_CLEANUP(dom_xml_serialization_algorithm(ctxt, out, &map, child, inherited_ns, prefix_index, indent));
1017 		}
1018 
1019 		if (should_format) {
1020 			indent--;
1021 			TRY_OR_CLEANUP(dom_xml_output_indents(out, indent));
1022 		}
1023 
1024 		/* 20. Append the following to markup, in the order listed: */
1025 		TRY_OR_CLEANUP(xmlOutputBufferWriteLit(out, "</"));
1026 		TRY_OR_CLEANUP(dom_xml_output_qname(out, &qualified_name));
1027 		TRY_OR_CLEANUP(xmlOutputBufferWriteLit(out, ">"));
1028 	}
1029 
1030 	/* 21. Return the value of markup.
1031 	 *     => We use the output buffer instead. */
1032 	dom_xml_ns_prefix_map_dtor(&map);
1033 	dom_xml_local_prefix_map_dtor(&local_prefixes_map);
1034 	return 0;
1035 
1036 cleanup:
1037 	dom_xml_ns_prefix_map_dtor(&map);
1038 	dom_xml_local_prefix_map_dtor(&local_prefixes_map);
1039 	return -1;
1040 }
1041 
1042 /* https://w3c.github.io/DOM-Parsing/#xml-serializing-a-documentfragment-node */
dom_xml_serializing_a_document_fragment_node(xmlSaveCtxtPtr ctxt,xmlOutputBufferPtr out,dom_xml_ns_prefix_map * namespace_prefix_map,xmlNodePtr node,const xmlChar * namespace,unsigned int * prefix_index,int indent)1043 static int dom_xml_serializing_a_document_fragment_node(
1044 	xmlSaveCtxtPtr ctxt,
1045 	xmlOutputBufferPtr out,
1046 	dom_xml_ns_prefix_map *namespace_prefix_map,
1047 	xmlNodePtr node,
1048 	const xmlChar *namespace,
1049 	unsigned int *prefix_index,
1050 	int indent
1051 )
1052 {
1053 	/* 1. Let markup the empty string.
1054 	 *    => We use the output buffer instead. */
1055 
1056 	/* 2. For each child child of node, in tree order, run the XML serialization algorithm on the child ... */
1057 	xmlNodePtr child = node->children;
1058 	while (child != NULL) {
1059 		TRY(dom_xml_serialization_algorithm(ctxt, out, namespace_prefix_map, child, namespace, prefix_index, indent));
1060 		child = child->next;
1061 	}
1062 
1063 	/* 3. Return the value of markup
1064 	 *    => We use the output buffer instead. */
1065 	return 0;
1066 }
1067 
1068 /* https://w3c.github.io/DOM-Parsing/#dfn-xml-serializing-a-document-node */
dom_xml_serializing_a_document_node(xmlSaveCtxtPtr ctxt,xmlOutputBufferPtr out,dom_xml_ns_prefix_map * namespace_prefix_map,xmlNodePtr node,const xmlChar * namespace,unsigned int * prefix_index,int indent)1069 static int dom_xml_serializing_a_document_node(
1070 	xmlSaveCtxtPtr ctxt,
1071 	xmlOutputBufferPtr out,
1072 	dom_xml_ns_prefix_map *namespace_prefix_map,
1073 	xmlNodePtr node,
1074 	const xmlChar *namespace,
1075 	unsigned int *prefix_index,
1076 	int indent
1077 )
1078 {
1079 	/* 1. Let serialized document be an empty string.
1080 	 *    => We use the output buffer instead. */
1081 
1082 	xmlNodePtr child = node->children;
1083 	node->children = NULL;
1084 
1085 	/* https://github.com/w3c/DOM-Parsing/issues/50 */
1086 	TRY(xmlOutputBufferFlush(out));
1087 	TRY(xmlSaveDoc(ctxt, (xmlDocPtr) node));
1088 	TRY(xmlSaveFlush(ctxt));
1089 
1090 	node->children = child;
1091 
1092 	/* 2. For each child child of node, in tree order, run the XML serialization algorithm on the child passing along the provided arguments,
1093 	 *    and append the result to serialized document. */
1094 	while (child != NULL) {
1095 		TRY(dom_xml_serialization_algorithm(ctxt, out, namespace_prefix_map, child, namespace, prefix_index, indent));
1096 		child = child->next;
1097 	}
1098 
1099 	/* 3. Return the value of serialized document.
1100 	 *    => We use the output buffer instead. */
1101 	return 0;
1102 }
1103 
1104 /* https://w3c.github.io/DOM-Parsing/#dfn-xml-serialization-algorithm */
dom_xml_serialization_algorithm(xmlSaveCtxtPtr ctxt,xmlOutputBufferPtr out,dom_xml_ns_prefix_map * namespace_prefix_map,xmlNodePtr node,const xmlChar * namespace,unsigned int * prefix_index,int indent)1105 static int dom_xml_serialization_algorithm(
1106 	xmlSaveCtxtPtr ctxt,
1107 	xmlOutputBufferPtr out,
1108 	dom_xml_ns_prefix_map *namespace_prefix_map,
1109 	xmlNodePtr node,
1110 	const xmlChar *namespace,
1111 	unsigned int *prefix_index,
1112 	int indent
1113 )
1114 {
1115 	/* If node's interface is: */
1116 	switch (node->type) {
1117 		case XML_ELEMENT_NODE:
1118 			return dom_xml_serialize_element_node(ctxt, out, namespace, namespace_prefix_map, node, prefix_index, indent);
1119 
1120 		case XML_DOCUMENT_FRAG_NODE:
1121 			return dom_xml_serializing_a_document_fragment_node(ctxt, out, namespace_prefix_map, node, namespace, prefix_index, indent);
1122 
1123 		case XML_HTML_DOCUMENT_NODE:
1124 		case XML_DOCUMENT_NODE:
1125 			return dom_xml_serializing_a_document_node(ctxt, out, namespace_prefix_map, node, namespace, prefix_index, indent);
1126 
1127 		case XML_TEXT_NODE:
1128 			return dom_xml_serialize_text_node(out, node);
1129 
1130 		case XML_COMMENT_NODE:
1131 			return dom_xml_serialize_comment_node(out, node);
1132 
1133 		case XML_PI_NODE:
1134 			return dom_xml_serialize_processing_instruction(out, node);
1135 
1136 		case XML_CDATA_SECTION_NODE:
1137 			return dom_xml_serialize_cdata_section_node(out, node);
1138 
1139 		case XML_ATTRIBUTE_NODE:
1140 			return dom_xml_serialize_attribute_node(out, node);
1141 
1142 		default:
1143 			TRY(xmlOutputBufferFlush(out));
1144 			TRY(xmlSaveTree(ctxt, node));
1145 			TRY(xmlSaveFlush(ctxt));
1146 			if (node->type == XML_DTD_NODE) {
1147 				return xmlOutputBufferWriteLit(out, "\n");
1148 			}
1149 			return 0;
1150 	}
1151 
1152 	ZEND_UNREACHABLE();
1153 }
1154 
1155 /* https://w3c.github.io/DOM-Parsing/#dfn-xml-serialization
1156  * Assumes well-formed == false. */
dom_xml_serialize(xmlSaveCtxtPtr ctxt,xmlOutputBufferPtr out,xmlNodePtr node,bool format)1157 int dom_xml_serialize(xmlSaveCtxtPtr ctxt, xmlOutputBufferPtr out, xmlNodePtr node, bool format)
1158 {
1159 	/* 1. Let namespace be a context namespace with value null. */
1160 	const xmlChar *namespace = NULL;
1161 
1162 	/* 2. Let prefix map be a new namespace prefix map. */
1163 	dom_xml_ns_prefix_map namespace_prefix_map;
1164 	dom_xml_ns_prefix_map_ctor(&namespace_prefix_map);
1165 
1166 	/* 3. Add the XML namespace with prefix value "xml" to prefix map. */
1167 	dom_xml_ns_prefix_map_add(&namespace_prefix_map, BAD_CAST "xml", false, BAD_CAST DOM_XML_NS_URI, strlen(DOM_XML_NS_URI));
1168 
1169 	/* 4. Let prefix index be a generated namespace prefix index with value 1. */
1170 	unsigned int prefix_index = 1;
1171 
1172 	/* 5. Return the result of running the XML serialization algorithm ... */
1173 	int indent = format ? 0 : -1;
1174 	int result = dom_xml_serialization_algorithm(ctxt, out, &namespace_prefix_map, node, namespace, &prefix_index, indent);
1175 
1176 	dom_xml_ns_prefix_map_dtor(&namespace_prefix_map);
1177 
1178 	return result;
1179 }
1180 
1181 #endif  /* HAVE_LIBXML && HAVE_DOM */
1182