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