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 "dom_properties.h"
25 #include "html5_parser.h"
26 #include "html5_serializer.h"
27 #include "xml_serializer.h"
28 #include "domexception.h"
29 #include <libxml/xmlsave.h>
30 #include <lexbor/dom/interfaces/element.h>
31 #include <lexbor/html/interfaces/document.h>
32 #include <lexbor/tag/tag.h>
33 #include <lexbor/encoding/encoding.h>
34
35 /* Spec date: 2024-04-14 */
36
dom_inner_html_write_string(void * application_data,const char * buf)37 static zend_result dom_inner_html_write_string(void *application_data, const char *buf)
38 {
39 smart_str *output = application_data;
40 smart_str_appends(output, buf);
41 return SUCCESS;
42 }
43
dom_inner_html_write_string_len(void * application_data,const char * buf,size_t len)44 static zend_result dom_inner_html_write_string_len(void *application_data, const char *buf, size_t len)
45 {
46 smart_str *output = application_data;
47 smart_str_appendl(output, buf, len);
48 return SUCCESS;
49 }
50
dom_write_smart_str(void * context,const char * buffer,int len)51 static int dom_write_smart_str(void *context, const char *buffer, int len)
52 {
53 smart_str *str = context;
54 smart_str_appendl(str, buffer, len);
55 return len;
56 }
57
58 /* https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#fragment-serializing-algorithm-steps */
dom_element_html_fragment_serialize(dom_object * obj,xmlNodePtr node)59 static zend_string *dom_element_html_fragment_serialize(dom_object *obj, xmlNodePtr node)
60 {
61 /* 1. Let context document be the value of node's node document. */
62 const xmlDoc *context_document = node->doc;
63
64 /* 2. If context document is an HTML document, return an HTML serialization of node. */
65 if (context_document->type == XML_HTML_DOCUMENT_NODE) {
66 smart_str output = {0};
67 dom_html5_serialize_context ctx;
68 ctx.private_data = php_dom_get_private_data(obj);
69 ctx.application_data = &output;
70 ctx.write_string = dom_inner_html_write_string;
71 ctx.write_string_len = dom_inner_html_write_string_len;
72 dom_html5_serialize(&ctx, node);
73 return smart_str_extract(&output);
74 }
75 /* 3. Otherwise, context document is an XML document; return an XML serialization of node passing the flag require well-formed. */
76 else {
77 ZEND_ASSERT(context_document->type == XML_DOCUMENT_NODE);
78
79 int status = -1;
80 smart_str str = {0};
81 /* No need to check buf's return value, as xmlSaveToBuffer() will fail instead. */
82 xmlSaveCtxtPtr ctxt = xmlSaveToIO(dom_write_smart_str, NULL, &str, "UTF-8", XML_SAVE_AS_XML);
83 if (EXPECTED(ctxt != NULL)) {
84 xmlCharEncodingHandlerPtr handler = xmlFindCharEncodingHandler("UTF-8");
85 xmlOutputBufferPtr out = xmlOutputBufferCreateIO(dom_write_smart_str, NULL, &str, handler);
86 if (EXPECTED(out != NULL)) {
87 php_dom_private_data *private_data = php_dom_get_private_data(obj);
88 /* Note: the innerHTML mixin sets the well-formed flag to true. */
89 xmlNodePtr child = node->children;
90 status = 0;
91 while (child != NULL && status == 0) {
92 status = dom_xml_serialize(ctxt, out, child, false, true, private_data);
93 child = child->next;
94 }
95 status |= xmlOutputBufferFlush(out);
96 status |= xmlOutputBufferClose(out);
97 }
98 (void) xmlSaveClose(ctxt);
99 xmlCharEncCloseFunc(handler);
100 }
101 if (UNEXPECTED(status < 0)) {
102 smart_str_free_ex(&str, false);
103 php_dom_throw_error_with_message(SYNTAX_ERR, "The resulting XML serialization is not well-formed", true);
104 return NULL;
105 }
106 return smart_str_extract(&str);
107 }
108 }
109
110 /* https://w3c.github.io/DOM-Parsing/#the-innerhtml-mixin */
dom_element_inner_html_read(dom_object * obj,zval * retval)111 zend_result dom_element_inner_html_read(dom_object *obj, zval *retval)
112 {
113 DOM_PROP_NODE(xmlNodePtr, node, obj);
114 zend_string *serialization = dom_element_html_fragment_serialize(obj, node);
115 if (serialization == NULL) {
116 return FAILURE;
117 }
118 ZVAL_STR(retval, serialization);
119 return SUCCESS;
120 }
121
dom_html_fragment_lexbor_parse(lxb_html_document_t * document,lxb_dom_element_t * element,const zend_string * input)122 static lxb_dom_node_t *dom_html_fragment_lexbor_parse(lxb_html_document_t *document, lxb_dom_element_t *element, const zend_string *input)
123 {
124 lxb_status_t status = lxb_html_document_parse_fragment_chunk_begin(document, element);
125 if (status != LXB_STATUS_OK) {
126 return NULL;
127 }
128
129 const lxb_encoding_data_t *encoding_data = lxb_encoding_data(LXB_ENCODING_UTF_8);
130 lxb_encoding_decode_t decode;
131 lxb_encoding_decode_init_single(&decode, encoding_data);
132
133 const lxb_char_t *buf_ref = (const lxb_char_t *) ZSTR_VAL(input);
134 if (ZSTR_IS_VALID_UTF8(input)) {
135 /* If we know the input is valid UTF-8, we don't have to perform checks and replace invalid sequences. */
136 status = lxb_html_document_parse_fragment_chunk(document, buf_ref, ZSTR_LEN(input));
137 if (UNEXPECTED(status != LXB_STATUS_OK)) {
138 return NULL;
139 }
140 } else {
141 /* See dom_decode_encode_fast_path(), simplified version for in-memory use-case. */
142 const lxb_char_t *buf_end = buf_ref + ZSTR_LEN(input);
143 const lxb_char_t *last_output = buf_ref;
144 while (buf_ref < buf_end) {
145 if (decode.u.utf_8.need == 0 && *buf_ref < 0x80) {
146 buf_ref++;
147 continue;
148 }
149
150 const lxb_char_t *buf_ref_backup = buf_ref;
151 lxb_codepoint_t codepoint = lxb_encoding_decode_utf_8_single(&decode, &buf_ref, buf_end);
152 if (UNEXPECTED(codepoint > LXB_ENCODING_MAX_CODEPOINT)) {
153 status = lxb_html_document_parse_fragment_chunk(document, last_output, buf_ref_backup - last_output);
154 if (UNEXPECTED(status != LXB_STATUS_OK)) {
155 return NULL;
156 }
157
158 status = lxb_html_document_parse_fragment_chunk(document, LXB_ENCODING_REPLACEMENT_BYTES, LXB_ENCODING_REPLACEMENT_SIZE);
159 if (UNEXPECTED(status != LXB_STATUS_OK)) {
160 return NULL;
161 }
162
163 last_output = buf_ref;
164 }
165 }
166
167 if (buf_ref != last_output) {
168 status = lxb_html_document_parse_fragment_chunk(document, last_output, buf_ref - last_output);
169 if (UNEXPECTED(status != LXB_STATUS_OK)) {
170 return NULL;
171 }
172 }
173 }
174
175 return lxb_html_document_parse_fragment_chunk_end(document);
176 }
177
dom_translate_quirks_mode(php_libxml_quirks_mode quirks_mode)178 static lxb_dom_document_cmode_t dom_translate_quirks_mode(php_libxml_quirks_mode quirks_mode)
179 {
180 switch (quirks_mode) {
181 case PHP_LIBXML_NO_QUIRKS: return LXB_DOM_DOCUMENT_CMODE_NO_QUIRKS;
182 case PHP_LIBXML_LIMITED_QUIRKS: return LXB_DOM_DOCUMENT_CMODE_LIMITED_QUIRKS;
183 case PHP_LIBXML_QUIRKS: return LXB_DOM_DOCUMENT_CMODE_QUIRKS;
184 EMPTY_SWITCH_DEFAULT_CASE();
185 }
186 }
187
188 /* https://html.spec.whatwg.org/#html-fragment-parsing-algorithm */
dom_html_fragment_parsing_algorithm(dom_object * obj,xmlNodePtr context_node,const zend_string * input,php_libxml_quirks_mode quirks_mode)189 static xmlNodePtr dom_html_fragment_parsing_algorithm(dom_object *obj, xmlNodePtr context_node, const zend_string *input, php_libxml_quirks_mode quirks_mode)
190 {
191 /* The whole algorithm is implemented in Lexbor, we just have to be the adapter between the
192 * data structures used in PHP and what Lexbor expects. */
193
194 lxb_html_document_t *document = lxb_html_document_create();
195 document->dom_document.compat_mode = dom_translate_quirks_mode(quirks_mode);
196 lxb_dom_element_t *element = lxb_dom_element_interface_create(&document->dom_document);
197
198 const lxb_tag_data_t *tag_data = lxb_tag_data_by_name(document->dom_document.tags, (lxb_char_t *) context_node->name, xmlStrlen(context_node->name));
199 element->node.local_name = tag_data == NULL ? LXB_TAG__UNDEF : tag_data->tag_id;
200
201 const lxb_char_t *ns_uri;
202 size_t ns_uri_len;
203 if (context_node->ns == NULL || context_node->ns->href == NULL) {
204 ns_uri = (lxb_char_t *) "";
205 ns_uri_len = 0;
206 } else {
207 ns_uri = context_node->ns->href;
208 ns_uri_len = xmlStrlen(ns_uri);
209 }
210 const lxb_ns_data_t *ns_data = lxb_ns_data_by_link(document->dom_document.ns, ns_uri, ns_uri_len);
211 element->node.ns = ns_data == NULL ? LXB_NS__UNDEF : ns_data->ns_id;
212
213 lxb_dom_node_t *node = dom_html_fragment_lexbor_parse(document, element, input);
214 xmlNodePtr fragment = NULL;
215 if (node != NULL) {
216 /* node->last_child could be NULL, but that is allowed. */
217 lexbor_libxml2_bridge_status status = lexbor_libxml2_bridge_convert_fragment(node->last_child, context_node->doc, &fragment, true, true, php_dom_get_private_data(obj));
218 if (UNEXPECTED(status != LEXBOR_LIBXML2_BRIDGE_STATUS_OK)) {
219 php_dom_throw_error(INVALID_STATE_ERR, true);
220 }
221 } else {
222 php_dom_throw_error(INVALID_STATE_ERR, true);
223 }
224
225 lxb_html_document_destroy(document);
226
227 return fragment;
228 }
229
dom_xml_parser_tag_name(const xmlNode * context_node,xmlParserCtxtPtr parser)230 static void dom_xml_parser_tag_name(const xmlNode *context_node, xmlParserCtxtPtr parser)
231 {
232 if (context_node->ns != NULL && context_node->ns->prefix != NULL) {
233 xmlParseChunk(parser, (const char *) context_node->ns->prefix, xmlStrlen(context_node->ns->prefix), 0);
234 xmlParseChunk(parser, ":", 1, 0);
235 }
236
237 xmlParseChunk(parser, (const char *) context_node->name, xmlStrlen(context_node->name), 0);
238 }
239
dom_xml_fragment_parsing_algorithm_parse(php_dom_libxml_ns_mapper * ns_mapper,const xmlNode * context_node,const zend_string * input,xmlParserCtxtPtr parser)240 static void dom_xml_fragment_parsing_algorithm_parse(php_dom_libxml_ns_mapper *ns_mapper, const xmlNode *context_node, const zend_string *input, xmlParserCtxtPtr parser)
241 {
242 xmlParseChunk(parser, "<", 1, 0);
243 dom_xml_parser_tag_name(context_node, parser);
244
245 /* Namespaces: we have to declare all in-scope namespaces including the default namespace */
246 /* xmlns attributes */
247 php_dom_in_scope_ns in_scope_ns = php_dom_get_in_scope_ns(ns_mapper, context_node, true);
248 for (size_t i = 0; i < in_scope_ns.count; i++) {
249 const xmlNs *ns = in_scope_ns.list[i];
250 xmlParseChunk(parser, " xmlns:", 7, 0);
251 ZEND_ASSERT(ns->prefix != NULL);
252 xmlParseChunk(parser, (const char *) ns->prefix, xmlStrlen(ns->prefix), 0);
253 xmlParseChunk(parser, "=\"", 2, 0);
254 xmlParseChunk(parser, (const char *) ns->href, xmlStrlen(ns->href), 0);
255 xmlParseChunk(parser, "\"", 1, 0);
256 }
257 php_dom_in_scope_ns_destroy(&in_scope_ns);
258 /* default namespace */
259 const char *default_ns = dom_locate_a_namespace(context_node, NULL);
260 if (default_ns != NULL) {
261 xmlParseChunk(parser, " xmlns=\"", 8, 0);
262 xmlParseChunk(parser, default_ns, strlen(default_ns), 0);
263 xmlParseChunk(parser, "\"", 1, 0);
264 }
265
266 xmlParseChunk(parser, ">", 1, 0);
267
268 xmlParseChunk(parser, (const char *) ZSTR_VAL(input), ZSTR_LEN(input), 0);
269
270 xmlParseChunk(parser, "</", 2, 0);
271 dom_xml_parser_tag_name(context_node, parser);
272 xmlParseChunk(parser, ">", 1, 1);
273 }
274
275 /* https://html.spec.whatwg.org/#xml-fragment-parsing-algorithm */
dom_xml_fragment_parsing_algorithm(dom_object * obj,const xmlNode * context_node,const zend_string * input)276 static xmlNodePtr dom_xml_fragment_parsing_algorithm(dom_object *obj, const xmlNode *context_node, const zend_string *input)
277 {
278 /* Steps 1-4 below */
279 xmlParserCtxtPtr parser = xmlCreatePushParserCtxt(NULL, NULL, NULL, 0, NULL);
280 if (UNEXPECTED(parser == NULL)) {
281 php_dom_throw_error(INVALID_STATE_ERR, true);
282 return NULL;
283 }
284
285 /* This is not only good to avoid a performance cost of changing the tree, but also to work around an old bug
286 * in xmlSetTreeDoc(). */
287 xmlDictFree(parser->dict);
288 if (context_node->doc->dict == NULL) {
289 context_node->doc->dict = xmlDictCreate();
290 xmlDictSetLimit(context_node->doc->dict, XML_MAX_DICTIONARY_LIMIT);
291 }
292 parser->dict = context_node->doc->dict;
293
294 php_libxml_sanitize_parse_ctxt_options(parser);
295 xmlCtxtUseOptions(parser, XML_PARSE_IGNORE_ENC | XML_PARSE_NOERROR | XML_PARSE_NOWARNING);
296
297 xmlCharEncodingHandlerPtr encoding = xmlFindCharEncodingHandler("UTF-8");
298 (void) xmlSwitchToEncoding(parser, encoding);
299
300 php_dom_libxml_ns_mapper *ns_mapper = php_dom_get_ns_mapper(obj);
301 dom_xml_fragment_parsing_algorithm_parse(ns_mapper, context_node, input, parser);
302
303 /* 5. If there is an XML well-formedness or XML namespace well-formedness error, then throw a "SyntaxError" DOMException. */
304 if (!parser->wellFormed || !parser->nsWellFormed) {
305 parser->dict = NULL;
306 xmlFreeDoc(parser->myDoc);
307 xmlFreeParserCtxt(parser);
308 php_dom_throw_error_with_message(SYNTAX_ERR, "XML fragment is not well-formed", true);
309 return NULL;
310 }
311
312 xmlDocPtr doc = parser->myDoc;
313 xmlFreeParserCtxt(parser);
314
315 if (EXPECTED(doc != NULL)) {
316 doc->dict = NULL;
317
318 /* 6. If the document element of the resulting Document has any sibling nodes, then throw a "SyntaxError" DOMException. */
319 xmlNodePtr document_element = doc->children;
320 if (document_element == NULL || document_element->next != NULL) {
321 xmlFreeDoc(doc);
322 php_dom_throw_error_with_message(SYNTAX_ERR, "XML fragment is not well-formed", true);
323 return NULL;
324 }
325
326 /* 7. Return the child nodes of the document element of the resulting Document, in tree order. */
327 xmlNodePtr fragment = xmlNewDocFragment(context_node->doc);
328 if (EXPECTED(fragment != NULL)) {
329 xmlNodePtr child = document_element->children;
330 /* Yes, we have to call both xmlSetTreeDoc() prior to xmlAddChildList()
331 * because xmlAddChildList() _only_ sets the tree for the topmost elements in the subtree! */
332 xmlSetTreeDoc(document_element, context_node->doc);
333 xmlAddChildList(fragment, child);
334 dom_mark_namespaces_as_attributes_too(ns_mapper, doc);
335 document_element->children = NULL;
336 document_element->last = NULL;
337 }
338 xmlFreeDoc(doc);
339 return fragment;
340 }
341 return NULL;
342 }
343
344 /* https://w3c.github.io/DOM-Parsing/#dfn-fragment-parsing-algorithm */
dom_parse_fragment(dom_object * obj,xmlNodePtr context_node,const zend_string * input)345 xmlNodePtr dom_parse_fragment(dom_object *obj, xmlNodePtr context_node, const zend_string *input)
346 {
347 if (context_node->doc->type == XML_DOCUMENT_NODE) {
348 return dom_xml_fragment_parsing_algorithm(obj, context_node, input);
349 } else {
350 return dom_html_fragment_parsing_algorithm(obj, context_node, input, obj->document->quirks_mode);
351 }
352 }
353
354 /* https://w3c.github.io/DOM-Parsing/#the-innerhtml-mixin */
dom_element_inner_html_write(dom_object * obj,zval * newval)355 zend_result dom_element_inner_html_write(dom_object *obj, zval *newval)
356 {
357 /* 1. We don't do injection sinks, skip. */
358
359 /* 2. Let context be this. */
360 DOM_PROP_NODE(xmlNodePtr, context_node, obj);
361
362 /* 3. Let fragment be the result of invoking the fragment parsing algorithm steps with context and compliantString. */
363 xmlNodePtr fragment = dom_parse_fragment(obj, context_node, Z_STR_P(newval));
364 if (fragment == NULL) {
365 return FAILURE;
366 }
367
368 /* 4. If context is a template element, then set context to the template element's template contents (a DocumentFragment). */
369 if (php_dom_ns_is_fast(context_node, php_dom_ns_is_html_magic_token) && xmlStrEqual(context_node->name, BAD_CAST "template")) {
370 context_node = php_dom_ensure_templated_content(php_dom_get_private_data(obj), context_node);
371 if (context_node == NULL) {
372 xmlFreeNode(fragment);
373 return FAILURE;
374 }
375 }
376
377 ZEND_ASSERT(obj->document != NULL);
378 php_libxml_invalidate_node_list_cache(obj->document);
379
380 /* 5. Replace all with fragment within context. */
381 dom_remove_all_children(context_node);
382 return php_dom_pre_insert(obj->document, fragment, context_node, NULL) ? SUCCESS : FAILURE;
383 }
384
385 /* https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#the-outerhtml-property */
dom_element_outer_html_read(dom_object * obj,zval * retval)386 zend_result dom_element_outer_html_read(dom_object *obj, zval *retval)
387 {
388 DOM_PROP_NODE(xmlNodePtr, this, obj);
389
390 /* 1. Let element be a fictional node whose only child is this. */
391 xmlNode element;
392 memset(&element, 0, sizeof(element));
393 element.type = XML_DOCUMENT_FRAG_NODE;
394 element.children = element.last = this;
395 element.doc = this->doc;
396
397 xmlNodePtr old_parent = this->parent;
398 xmlNodePtr old_next = this->next;
399 this->parent = &element;
400 this->next = NULL;
401
402 /* 2. Return the result of running fragment serializing algorithm steps with element and true. */
403 zend_string *serialization = dom_element_html_fragment_serialize(obj, &element);
404
405 this->parent = old_parent;
406 this->next = old_next;
407
408 if (serialization == NULL) {
409 return FAILURE;
410 }
411 ZVAL_STR(retval, serialization);
412 return SUCCESS;
413 }
414
415 /* https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#the-outerhtml-property */
dom_element_outer_html_write(dom_object * obj,zval * newval)416 zend_result dom_element_outer_html_write(dom_object *obj, zval *newval)
417 {
418 /* 1. We don't do injection sinks, skip. */
419
420 /* 2. Let parent be this's parent. */
421 DOM_PROP_NODE(xmlNodePtr, this, obj);
422 xmlNodePtr parent = this->parent;
423 bool created_parent = false;
424
425 /* 3. If parent is null, return. */
426 if (parent == NULL) {
427 return SUCCESS;
428 }
429
430 /* 4. If parent is a Document, throw. */
431 if (parent->type == XML_DOCUMENT_NODE || parent->type == XML_HTML_DOCUMENT_NODE) {
432 php_dom_throw_error(INVALID_MODIFICATION_ERR, true);
433 return FAILURE;
434 }
435
436 /* 5. If parent is a DocumentFragment, set parent to the result of creating an element given this's node document, body, and the HTML namespace. */
437 if (parent->type == XML_DOCUMENT_FRAG_NODE) {
438 xmlNsPtr html_ns = php_dom_libxml_ns_mapper_ensure_html_ns(php_dom_get_ns_mapper(obj));
439
440 parent = xmlNewDocNode(parent->doc, html_ns, BAD_CAST "body", NULL);
441 created_parent = true;
442 if (UNEXPECTED(parent == NULL)) {
443 php_dom_throw_error(INVALID_STATE_ERR, true);
444 return FAILURE;
445 }
446 }
447
448 /* 6. Let fragment be the result of invoking the fragment parsing algorithm steps given parent and compliantString. */
449 xmlNodePtr fragment = dom_parse_fragment(obj, parent, Z_STR_P(newval));
450 if (fragment == NULL) {
451 if (created_parent) {
452 xmlFreeNode(parent);
453 }
454 return FAILURE;
455 }
456
457 ZEND_ASSERT(obj->document != NULL);
458 php_libxml_invalidate_node_list_cache(obj->document);
459
460 /* 7. Replace this with fragment within this's parent. */
461 if (!php_dom_pre_insert(obj->document, fragment, this->parent, this)) {
462 xmlFreeNode(fragment);
463 if (created_parent) {
464 xmlFreeNode(parent);
465 }
466 return FAILURE;
467 }
468 xmlUnlinkNode(this);
469 if (created_parent) {
470 ZEND_ASSERT(parent->children == NULL);
471 xmlFreeNode(parent);
472 }
473 return SUCCESS;
474 }
475
476 #endif
477