diff --git a/NEWS b/NEWS index d51561dd8e909..e238670d296eb 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,9 @@ PHP NEWS - Core: . Fixed bug GH-14801 (Fix build for armv7). (andypost) +- DOM: + . Improve support for template elements. (nielsdos) + - GD: . Check overflow/underflow for imagescale/imagefilter. (David Carlier) diff --git a/ext/dom/config.m4 b/ext/dom/config.m4 index f16804d468ba6..8bb41d9ee8118 100644 --- a/ext/dom/config.m4 +++ b/ext/dom/config.m4 @@ -27,7 +27,7 @@ if test "$PHP_DOM" != "no"; then $LEXBOR_DIR/ns/ns.c \ $LEXBOR_DIR/tag/tag.c" PHP_NEW_EXTENSION(dom, [php_dom.c attr.c document.c infra.c \ - xml_document.c html_document.c xml_serializer.c html5_serializer.c html5_parser.c namespace_compat.c \ + xml_document.c html_document.c xml_serializer.c html5_serializer.c html5_parser.c namespace_compat.c private_data.c \ domexception.c \ parentnode/tree.c parentnode/css_selectors.c \ processinginstruction.c cdatasection.c \ diff --git a/ext/dom/config.w32 b/ext/dom/config.w32 index 081190a67f047..8035c5b84f0fc 100644 --- a/ext/dom/config.w32 +++ b/ext/dom/config.w32 @@ -8,7 +8,7 @@ if (PHP_DOM == "yes") { CHECK_HEADER_ADD_INCLUDE("libxml/parser.h", "CFLAGS_DOM", PHP_PHP_BUILD + "\\include\\libxml2") ) { EXTENSION("dom", "php_dom.c attr.c document.c infra.c \ - xml_document.c html_document.c xml_serializer.c html5_serializer.c html5_parser.c namespace_compat.c \ + xml_document.c html_document.c xml_serializer.c html5_serializer.c html5_parser.c namespace_compat.c private_data.c \ domexception.c processinginstruction.c \ cdatasection.c documentfragment.c domimplementation.c element.c inner_html_mixin.c \ node.c characterdata.c documenttype.c \ diff --git a/ext/dom/document.c b/ext/dom/document.c index 7162550015d29..e315255b43812 100644 --- a/ext/dom/document.c +++ b/ext/dom/document.c @@ -23,6 +23,7 @@ #if defined(HAVE_LIBXML) && defined(HAVE_DOM) #include "php_dom.h" #include "namespace_compat.h" +#include "private_data.h" #include "xml_serializer.h" #include "internal_helpers.h" #include "dom_properties.h" diff --git a/ext/dom/domimplementation.c b/ext/dom/domimplementation.c index 58a50b06d074e..0c2b2c61b308d 100644 --- a/ext/dom/domimplementation.c +++ b/ext/dom/domimplementation.c @@ -23,6 +23,7 @@ #if defined(HAVE_LIBXML) && defined(HAVE_DOM) #include "php_dom.h" #include "namespace_compat.h" +#include "private_data.h" /* * class DOMImplementation @@ -266,7 +267,8 @@ PHP_METHOD(Dom_Implementation, createDocument) xmlDocPtr document = NULL; xmlChar *localname = NULL, *prefix = NULL; - php_dom_libxml_ns_mapper *ns_mapper = php_dom_libxml_ns_mapper_create(); + php_dom_private_data *private_data = php_dom_private_data_create(); + php_dom_libxml_ns_mapper *ns_mapper = php_dom_ns_mapper_from_private(private_data); /* 1. Let document be a new XMLDocument. */ document = xmlNewDoc(BAD_CAST "1.0"); @@ -307,7 +309,7 @@ PHP_METHOD(Dom_Implementation, createDocument) NULL ); dom_set_xml_class(intern->document); - intern->document->private_data = php_dom_libxml_ns_mapper_header(ns_mapper); + intern->document->private_data = php_dom_libxml_private_data_header(private_data); /* 4. If doctype is non-null, append doctype to document. */ if (doctype != NULL) { @@ -337,7 +339,7 @@ PHP_METHOD(Dom_Implementation, createDocument) xmlFree(localname); xmlFree(prefix); xmlFreeDoc(document); - php_dom_libxml_ns_mapper_destroy(ns_mapper); + php_dom_private_data_destroy(private_data); RETURN_THROWS(); } /* }}} end dom_domimplementation_create_document */ @@ -366,7 +368,8 @@ PHP_METHOD(Dom_Implementation, createHTMLDocument) /* 3. Append a new doctype, with "html" as its name and with its node document set to doc, to doc. */ xmlDtdPtr dtd = xmlCreateIntSubset(doc, BAD_CAST "html", NULL, NULL); - php_dom_libxml_ns_mapper *ns_mapper = php_dom_libxml_ns_mapper_create(); + php_dom_private_data *private_data = php_dom_private_data_create(); + php_dom_libxml_ns_mapper *ns_mapper = php_dom_ns_mapper_from_private(private_data); xmlNsPtr html_ns = php_dom_libxml_ns_mapper_ensure_html_ns(ns_mapper); /* 4. Append the result of creating an element given doc, html, and the HTML namespace, to doc. */ @@ -396,7 +399,7 @@ PHP_METHOD(Dom_Implementation, createHTMLDocument) if (UNEXPECTED(dtd == NULL || html_element == NULL || head_element == NULL || (title != NULL && title_element == NULL) || body_element == NULL)) { php_dom_throw_error(INVALID_STATE_ERR, true); xmlFreeDoc(doc); - php_dom_libxml_ns_mapper_destroy(ns_mapper); + php_dom_private_data_destroy(private_data); RETURN_THROWS(); } @@ -408,7 +411,7 @@ PHP_METHOD(Dom_Implementation, createHTMLDocument) NULL ); dom_set_xml_class(intern->document); - intern->document->private_data = php_dom_libxml_ns_mapper_header(ns_mapper); + intern->document->private_data = php_dom_libxml_private_data_header(private_data); } /* }}} */ diff --git a/ext/dom/element.c b/ext/dom/element.c index 9795c0bd7aa8f..0e1de94c46c69 100644 --- a/ext/dom/element.c +++ b/ext/dom/element.c @@ -24,6 +24,7 @@ #include "zend_enum.h" #include "php_dom.h" #include "namespace_compat.h" +#include "private_data.h" #include "internal_helpers.h" #include "dom_properties.h" #include "token_list.h" @@ -2030,6 +2031,16 @@ PHP_METHOD(Dom_Element, rename) } goto cleanup; } + + /* If we currently have a template but the new element type won't be a template, then throw away the templated content. */ + if (is_currently_html_ns && xmlStrEqual(nodep->name, BAD_CAST "template") && !xmlStrEqual(localname, BAD_CAST "template")) { + php_dom_throw_error_with_message( + INVALID_MODIFICATION_ERR, + "It is not possible to rename the template element because it hosts a document fragment", + /* strict */ true + ); + goto cleanup; + } } php_libxml_invalidate_node_list_cache(intern->document); diff --git a/ext/dom/html5_parser.c b/ext/dom/html5_parser.c index 66c490b1d169f..0d7d2b9e7249d 100644 --- a/ext/dom/html5_parser.c +++ b/ext/dom/html5_parser.c @@ -22,12 +22,13 @@ #if defined(HAVE_LIBXML) && defined(HAVE_DOM) #include "php_dom.h" #include "html5_parser.h" +#include "private_data.h" #include #include +#include #include #include #include -#include #define WORK_LIST_INIT_SIZE 128 /* libxml2 reserves 2 pointer-sized words for interned strings */ @@ -63,14 +64,20 @@ static unsigned short sanitize_line_nr(size_t line) return (unsigned short) line; } -static const php_dom_ns_magic_token *get_libxml_namespace_href(uintptr_t lexbor_namespace) +struct lxml_ns { + const php_dom_ns_magic_token *token; + const char *href; + size_t href_len; +}; + +static struct lxml_ns get_libxml_namespace_href(uintptr_t lexbor_namespace) { if (lexbor_namespace == LXB_NS_SVG) { - return php_dom_ns_is_svg_magic_token; + return (struct lxml_ns) { php_dom_ns_is_svg_magic_token, ZEND_STRL(DOM_SVG_NS_URI) }; } else if (lexbor_namespace == LXB_NS_MATH) { - return php_dom_ns_is_mathml_magic_token; + return (struct lxml_ns) { php_dom_ns_is_mathml_magic_token, ZEND_STRL(DOM_MATHML_NS_URI) }; } else { - return php_dom_ns_is_html_magic_token; + return (struct lxml_ns) { php_dom_ns_is_html_magic_token, ZEND_STRL(DOM_XHTML_NS_URI) }; } } @@ -102,11 +109,12 @@ static lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert( xmlNodePtr root, bool compact_text_nodes, bool create_default_ns, - php_dom_libxml_ns_mapper *ns_mapper + php_dom_private_data *private_data ) { lexbor_libxml2_bridge_status retval = LEXBOR_LIBXML2_BRIDGE_STATUS_OK; + php_dom_libxml_ns_mapper *ns_mapper = php_dom_ns_mapper_from_private(private_data); xmlNsPtr html_ns = php_dom_libxml_ns_mapper_ensure_html_ns(ns_mapper); xmlNsPtr xlink_ns = NULL; xmlNsPtr prefixed_xmlns_ns = NULL; @@ -146,24 +154,47 @@ static lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert( if (entering_namespace == LXB_NS_HTML) { current_lxml_ns = html_ns; } else { - const php_dom_ns_magic_token *magic_token = get_libxml_namespace_href(entering_namespace); - zend_string *uri = zend_string_init((char *) magic_token, strlen((char *) magic_token), false); + struct lxml_ns ns = get_libxml_namespace_href(entering_namespace); + zend_string *uri = zend_string_init(ns.href, ns.href_len, false); current_lxml_ns = php_dom_libxml_ns_mapper_get_ns(ns_mapper, NULL, uri); zend_string_release_ex(uri, false); if (EXPECTED(current_lxml_ns != NULL)) { - current_lxml_ns->_private = (void *) magic_token; + current_lxml_ns->_private = (void *) ns.token; } } } /* Instead of xmlSetNs() because we know the arguments are valid. Prevents overhead. */ lxml_element->ns = current_lxml_ns; - for (lxb_dom_node_t *child_node = element->node.last_child; child_node != NULL; child_node = child_node->prev) { + /* Handle template element by creating a fragment node to contain its children. + * Other types of nodes contain their children directly. */ + xmlNodePtr lxml_child_parent = lxml_element; + lxb_dom_node_t *child_node = element->node.last_child; + if (lxb_html_tree_node_is(&element->node, LXB_TAG_TEMPLATE)) { + if (create_default_ns) { + lxml_child_parent = xmlNewDocFragment(lxml_doc); + if (UNEXPECTED(lxml_child_parent == NULL)) { + retval = LEXBOR_LIBXML2_BRIDGE_STATUS_OOM; + break; + } + + lxml_child_parent->parent = lxml_element; + dom_add_element_ns_hook(private_data, lxml_element); + php_dom_add_templated_content(private_data, lxml_element, lxml_child_parent); + } + + lxb_html_template_element_t *template = lxb_html_interface_template(&element->node); + if (template->content != NULL) { + child_node = template->content->node.last_child; + } + } + + for (; child_node != NULL; child_node = child_node->prev) { lexbor_libxml2_bridge_work_list_item_push( &work_list, child_node, entering_namespace, - lxml_element, + lxml_child_parent, current_lxml_ns ); } @@ -307,7 +338,7 @@ lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert_document( xmlDocPtr *doc_out, bool compact_text_nodes, bool create_default_ns, - php_dom_libxml_ns_mapper *ns_mapper + php_dom_private_data *private_data ) { xmlDocPtr lxml_doc = php_dom_create_html_doc(); @@ -320,7 +351,7 @@ lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert_document( (xmlNodePtr) lxml_doc, compact_text_nodes, create_default_ns, - ns_mapper + private_data ); if (status != LEXBOR_LIBXML2_BRIDGE_STATUS_OK) { xmlFreeDoc(lxml_doc); @@ -336,7 +367,7 @@ lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert_fragment( xmlNodePtr *fragment_out, bool compact_text_nodes, bool create_default_ns, - php_dom_libxml_ns_mapper *ns_mapper + php_dom_private_data *private_data ) { xmlNodePtr fragment = xmlNewDocFragment(lxml_doc); @@ -349,7 +380,7 @@ lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert_fragment( fragment, compact_text_nodes, create_default_ns, - ns_mapper + private_data ); if (status != LEXBOR_LIBXML2_BRIDGE_STATUS_OK) { xmlFreeNode(fragment); diff --git a/ext/dom/html5_parser.h b/ext/dom/html5_parser.h index bfa6223f6a534..a56166639a602 100644 --- a/ext/dom/html5_parser.h +++ b/ext/dom/html5_parser.h @@ -71,7 +71,7 @@ lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert_document( xmlDocPtr *doc_out, bool compact_text_nodes, bool create_default_ns, - php_dom_libxml_ns_mapper *ns_mapper + php_dom_private_data *private_data ); lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert_fragment( lxb_dom_node_t *start_node, @@ -79,7 +79,7 @@ lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert_fragment( xmlNodePtr *fragment_out, bool compact_text_nodes, bool create_default_ns, - php_dom_libxml_ns_mapper *ns_mapper + php_dom_private_data *private_data ); void lexbor_libxml2_bridge_report_errors( const lexbor_libxml2_bridge_parse_context *ctx, diff --git a/ext/dom/html5_serializer.c b/ext/dom/html5_serializer.c index 215acb6da9543..c87d3480a5f3c 100644 --- a/ext/dom/html5_serializer.c +++ b/ext/dom/html5_serializer.c @@ -289,9 +289,13 @@ static zend_result dom_html5_serialize_node(dom_html5_serialize_context *ctx, co case XML_ELEMENT_NODE: { TRY(dom_html5_serialize_element_start(ctx, node)); - if (node->children) { + const xmlNode *children = node->children; + if (php_dom_ns_is_fast(node, php_dom_ns_is_html_magic_token) && xmlStrEqual(node->name, BAD_CAST "template")) { + children = php_dom_retrieve_templated_content(ctx->private_data, node); + } + if (children) { if (!dom_html5_serializes_as_void(node)) { - node = node->children; + node = children; continue; } } else { @@ -301,6 +305,14 @@ static zend_result dom_html5_serialize_node(dom_html5_serialize_context *ctx, co break; } + case XML_DOCUMENT_FRAG_NODE: { + if (node->children) { + node = node->children; + continue; + } + break; + } + /* Only exists for compatibility with XML and old DOM. */ case XML_ENTITY_REF_NODE: { TRY(dom_html5_serialize_entity_ref(ctx, node)); @@ -346,10 +358,15 @@ zend_result dom_html5_serialize(dom_html5_serialize_context *ctx, const xmlNode } /* Step 2 not needed because we're not using a string to store the serialized data */ - /* Step 3 not needed because we don't support template contents yet */ + + /* Step 3. If the node is a template element, then let the node instead be the template element's template contents (a DocumentFragment node). */ + xmlNodePtr children = php_dom_retrieve_templated_content(ctx->private_data, node); + if (!children) { + children = node->children; + } /* Step 4 */ - return dom_html5_serialize_node(ctx, node->children, node); + return dom_html5_serialize_node(ctx, children, node); } /* Variant on the above that is equivalent to the "outer HTML". */ diff --git a/ext/dom/html5_serializer.h b/ext/dom/html5_serializer.h index 8ddedd48495ab..27fd1c92f24fb 100644 --- a/ext/dom/html5_serializer.h +++ b/ext/dom/html5_serializer.h @@ -19,11 +19,13 @@ #include #include +#include "private_data.h" typedef struct { zend_result (*write_string)(void *application_data, const char *buf); zend_result (*write_string_len)(void *application_data, const char *buf, size_t len); void *application_data; + php_dom_private_data *private_data; } dom_html5_serialize_context; zend_result dom_html5_serialize(dom_html5_serialize_context *ctx, const xmlNode *node); diff --git a/ext/dom/html_document.c b/ext/dom/html_document.c index c8a805318c356..d094fe0a1bcbc 100644 --- a/ext/dom/html_document.c +++ b/ext/dom/html_document.c @@ -25,6 +25,7 @@ #include "html5_parser.h" #include "html5_serializer.h" #include "namespace_compat.h" +#include "private_data.h" #include "dom_properties.h" #include #include @@ -757,7 +758,7 @@ PHP_METHOD(Dom_HTMLDocument, createEmpty) NULL ); dom_set_xml_class(intern->document); - intern->document->private_data = php_dom_libxml_ns_mapper_header(php_dom_libxml_ns_mapper_create()); + intern->document->private_data = php_dom_libxml_private_data_header(php_dom_private_data_create()); return; oom: @@ -878,7 +879,7 @@ PHP_METHOD(Dom_HTMLDocument, createFromString) goto fail_oom; } - php_dom_libxml_ns_mapper *ns_mapper = php_dom_libxml_ns_mapper_create(); + php_dom_private_data *private_data = php_dom_private_data_create(); xmlDocPtr lxml_doc; lexbor_libxml2_bridge_status bridge_status = lexbor_libxml2_bridge_convert_document( @@ -886,11 +887,11 @@ PHP_METHOD(Dom_HTMLDocument, createFromString) &lxml_doc, options & XML_PARSE_COMPACT, !(options & DOM_HTML_NO_DEFAULT_NS), - ns_mapper + private_data ); lexbor_libxml2_bridge_copy_observations(parser->tree, &ctx.observations); if (UNEXPECTED(bridge_status != LEXBOR_LIBXML2_BRIDGE_STATUS_OK)) { - php_dom_libxml_ns_mapper_destroy(ns_mapper); + php_dom_private_data_destroy(private_data); php_libxml_ctx_error( NULL, "%s in %s", @@ -918,7 +919,7 @@ PHP_METHOD(Dom_HTMLDocument, createFromString) ); dom_set_xml_class(intern->document); intern->document->quirks_mode = ctx.observations.quirks_mode; - intern->document->private_data = php_dom_libxml_ns_mapper_header(ns_mapper); + intern->document->private_data = php_dom_libxml_private_data_header(private_data); return; fail_oom: @@ -930,7 +931,7 @@ PHP_METHOD(Dom_HTMLDocument, createFromString) PHP_METHOD(Dom_HTMLDocument, createFromFile) { const char *filename, *override_encoding = NULL; - php_dom_libxml_ns_mapper *ns_mapper = NULL; + php_dom_private_data *private_data = NULL; size_t filename_len, override_encoding_len; zend_long options = 0; php_stream *stream = NULL; @@ -1069,7 +1070,7 @@ PHP_METHOD(Dom_HTMLDocument, createFromFile) goto fail_oom; } - ns_mapper = php_dom_libxml_ns_mapper_create(); + private_data = php_dom_private_data_create(); xmlDocPtr lxml_doc; lexbor_libxml2_bridge_status bridge_status = lexbor_libxml2_bridge_convert_document( @@ -1077,7 +1078,7 @@ PHP_METHOD(Dom_HTMLDocument, createFromFile) &lxml_doc, options & XML_PARSE_COMPACT, !(options & DOM_HTML_NO_DEFAULT_NS), - ns_mapper + private_data ); lexbor_libxml2_bridge_copy_observations(parser->tree, &ctx.observations); if (UNEXPECTED(bridge_status != LEXBOR_LIBXML2_BRIDGE_STATUS_OK)) { @@ -1139,14 +1140,14 @@ PHP_METHOD(Dom_HTMLDocument, createFromFile) ); dom_set_xml_class(intern->document); intern->document->quirks_mode = ctx.observations.quirks_mode; - intern->document->private_data = php_dom_libxml_ns_mapper_header(ns_mapper); + intern->document->private_data = php_dom_libxml_private_data_header(private_data); return; fail_oom: php_dom_throw_error(INVALID_STATE_ERR, true); fail_general: - if (ns_mapper != NULL) { - php_dom_libxml_ns_mapper_destroy(ns_mapper); + if (private_data != NULL) { + php_dom_private_data_destroy(private_data); } lxb_html_document_destroy(document); php_stream_close(stream); @@ -1204,7 +1205,7 @@ static zend_result dom_saveHTML_write_string(void *application_data, const char return dom_saveHTML_write_string_len(application_data, buf, strlen(buf)); } -static zend_result dom_common_save(dom_output_ctx *output_ctx, const xmlDoc *docp, const xmlNode *node) +static zend_result dom_common_save(dom_output_ctx *output_ctx, dom_object *intern, const xmlDoc *docp, const xmlNode *node) { /* Initialize everything related to encoding & decoding */ const lxb_encoding_data_t *decoding_data = lxb_encoding_data(LXB_ENCODING_UTF_8); @@ -1237,6 +1238,7 @@ static zend_result dom_common_save(dom_output_ctx *output_ctx, const xmlDoc *doc ctx.write_string_len = dom_saveHTML_write_string_len; ctx.write_string = dom_saveHTML_write_string; ctx.application_data = output_ctx; + ctx.private_data = php_dom_get_private_data(intern); if (UNEXPECTED(dom_html5_serialize_outer(&ctx, node) != SUCCESS)) { return FAILURE; } @@ -1295,7 +1297,7 @@ PHP_METHOD(Dom_HTMLDocument, saveHtmlFile) dom_output_ctx output_ctx; output_ctx.output_data = stream; output_ctx.write_output = dom_write_output_stream; - if (UNEXPECTED(dom_common_save(&output_ctx, docp, (const xmlNode *) docp) != SUCCESS)) { + if (UNEXPECTED(dom_common_save(&output_ctx, intern, docp, (const xmlNode *) docp) != SUCCESS)) { php_stream_close(stream); RETURN_FALSE; } @@ -1334,7 +1336,7 @@ PHP_METHOD(Dom_HTMLDocument, saveHtml) output_ctx.output_data = &buf; output_ctx.write_output = dom_write_output_smart_str; /* Can't fail because dom_write_output_smart_str() can't fail. */ - zend_result result = dom_common_save(&output_ctx, docp, node); + zend_result result = dom_common_save(&output_ctx, intern, docp, node); ZEND_ASSERT(result == SUCCESS); RETURN_STR(smart_str_extract(&buf)); @@ -1642,4 +1644,19 @@ zend_result dom_html_document_title_write(dom_object *obj, zval *newval) return SUCCESS; } +#if ZEND_DEBUG +PHP_METHOD(Dom_HTMLDocument, debugGetTemplateCount) +{ + xmlDocPtr doc; + dom_object *intern; + + ZEND_PARSE_PARAMETERS_NONE(); + + DOM_GET_OBJ(doc, ZEND_THIS, xmlDocPtr, intern); + ZEND_IGNORE_VALUE(doc); + + RETURN_LONG((zend_long) php_dom_get_template_count((const php_dom_private_data *) intern->document->private_data)); +} +#endif + #endif /* HAVE_LIBXML && HAVE_DOM */ diff --git a/ext/dom/inner_html_mixin.c b/ext/dom/inner_html_mixin.c index 6db71462d1aee..651a71bace2b3 100644 --- a/ext/dom/inner_html_mixin.c +++ b/ext/dom/inner_html_mixin.c @@ -68,6 +68,7 @@ zend_result dom_element_inner_html_read(dom_object *obj, zval *retval) if (context_document->type == XML_HTML_DOCUMENT_NODE) { smart_str output = {0}; dom_html5_serialize_context ctx; + ctx.private_data = php_dom_get_private_data(obj); ctx.application_data = &output; ctx.write_string = dom_inner_html_write_string; ctx.write_string_len = dom_inner_html_write_string_len; @@ -86,11 +87,12 @@ zend_result dom_element_inner_html_read(dom_object *obj, zval *retval) xmlCharEncodingHandlerPtr handler = xmlFindCharEncodingHandler("UTF-8"); xmlOutputBufferPtr out = xmlOutputBufferCreateIO(dom_write_smart_str, NULL, &str, handler); if (EXPECTED(out != NULL)) { + php_dom_private_data *private_data = php_dom_get_private_data(obj); /* Note: the innerHTML mixin sets the well-formed flag to true. */ xmlNodePtr child = node->children; status = 0; while (child != NULL && status == 0) { - status = dom_xml_serialize(ctxt, out, child, false, true); + status = dom_xml_serialize(ctxt, out, child, false, true, private_data); child = child->next; } status |= xmlOutputBufferFlush(out); @@ -205,7 +207,7 @@ static xmlNodePtr dom_html_fragment_parsing_algorithm(dom_object *obj, xmlNodePt xmlNodePtr fragment = NULL; if (node != NULL) { /* node->last_child could be NULL, but that is allowed. */ - lexbor_libxml2_bridge_status status = lexbor_libxml2_bridge_convert_fragment(node->last_child, context_node->doc, &fragment, true, true, php_dom_get_ns_mapper(obj)); + 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)); if (UNEXPECTED(status != LEXBOR_LIBXML2_BRIDGE_STATUS_OK)) { php_dom_throw_error(INVALID_STATE_ERR, true); } @@ -349,6 +351,14 @@ zend_result dom_element_inner_html_write(dom_object *obj, zval *newval) return FAILURE; } + if (php_dom_ns_is_fast(context_node, php_dom_ns_is_html_magic_token) && xmlStrEqual(context_node->name, BAD_CAST "template")) { + context_node = php_dom_ensure_templated_content(php_dom_get_private_data(obj), context_node); + if (context_node == NULL) { + xmlFreeNode(fragment); + return FAILURE; + } + } + /* We skip the steps involving the template element as context node since we don't do special handling for that. */ dom_remove_all_children(context_node); return php_dom_pre_insert(obj->document, fragment, context_node, NULL) ? SUCCESS : FAILURE; diff --git a/ext/dom/internal_helpers.h b/ext/dom/internal_helpers.h index 95f49d5f480b6..c87ea49c61438 100644 --- a/ext/dom/internal_helpers.h +++ b/ext/dom/internal_helpers.h @@ -89,4 +89,12 @@ static zend_always_inline bool dom_is_document_cache_modified_since_parsing(php_ return !doc_ptr || doc_ptr->cache_tag.modification_nr > dom_minimum_modification_nr_since_parsing(doc_ptr); } +static zend_always_inline zend_long dom_mangle_pointer_for_key(const void *ptr) +{ + zend_ulong value = (zend_ulong) (uintptr_t) ptr; + /* Rotate 3/4 bits for better hash distribution because the low 3/4 bits are normally 0. */ + const size_t rol_amount = (SIZEOF_ZEND_LONG == 8) ? 4 : 3; + return (value >> rol_amount) | (value << (sizeof(value) * 8 - rol_amount)); +} + #endif diff --git a/ext/dom/namespace_compat.c b/ext/dom/namespace_compat.c index 51df10e2e087b..7a3bd68b0111a 100644 --- a/ext/dom/namespace_compat.c +++ b/ext/dom/namespace_compat.c @@ -22,23 +22,25 @@ #if defined(HAVE_LIBXML) && defined(HAVE_DOM) #include "php_dom.h" #include "namespace_compat.h" +#include "private_data.h" #include "internal_helpers.h" -PHP_DOM_EXPORT const php_dom_ns_magic_token *php_dom_ns_is_html_magic_token = (const php_dom_ns_magic_token *) DOM_XHTML_NS_URI; -PHP_DOM_EXPORT const php_dom_ns_magic_token *php_dom_ns_is_mathml_magic_token = (const php_dom_ns_magic_token *) DOM_MATHML_NS_URI; -PHP_DOM_EXPORT const php_dom_ns_magic_token *php_dom_ns_is_svg_magic_token = (const php_dom_ns_magic_token *) DOM_SVG_NS_URI; -PHP_DOM_EXPORT const php_dom_ns_magic_token *php_dom_ns_is_xlink_magic_token = (const php_dom_ns_magic_token *) DOM_XLINK_NS_URI; -PHP_DOM_EXPORT const php_dom_ns_magic_token *php_dom_ns_is_xml_magic_token = (const php_dom_ns_magic_token *) DOM_XML_NS_URI; -PHP_DOM_EXPORT const php_dom_ns_magic_token *php_dom_ns_is_xmlns_magic_token = (const php_dom_ns_magic_token *) DOM_XMLNS_NS_URI; - -struct php_dom_libxml_ns_mapper { - php_libxml_private_data_header header; - /* This is used almost all the time for HTML documents, so it makes sense to cache this. */ - xmlNsPtr html_ns; - /* Used for every prefixless namespace declaration in XML, so also very common. */ - xmlNsPtr prefixless_xmlns_ns; - HashTable uri_to_prefix_map; -}; +/* The actual value of these doesn't matter as long as they serve as a unique ID. + * They need to be pointers because the `_private` field is a pointer, however we can choose the contents ourselves. + * We need keep these at least 4-byte aligned because the pointer may be tagged (although for now 2 byte alignment works too). + * We use a trick: we declare a struct with a double member to force the alignment. */ +#define DECLARE_NS_TOKEN(name, uri) \ + static const struct { \ + char val[sizeof(uri)]; \ + double align; \ + } decl_##name = { uri, 0.0 }; \ + PHP_DOM_EXPORT const php_dom_ns_magic_token *(name) = (const php_dom_ns_magic_token *) &decl_##name; +DECLARE_NS_TOKEN(php_dom_ns_is_html_magic_token, DOM_XHTML_NS_URI); +DECLARE_NS_TOKEN(php_dom_ns_is_mathml_magic_token, DOM_MATHML_NS_URI); +DECLARE_NS_TOKEN(php_dom_ns_is_svg_magic_token, DOM_SVG_NS_URI); +DECLARE_NS_TOKEN(php_dom_ns_is_xlink_magic_token, DOM_XLINK_NS_URI); +DECLARE_NS_TOKEN(php_dom_ns_is_xml_magic_token, DOM_XML_NS_URI); +DECLARE_NS_TOKEN(php_dom_ns_is_xmlns_magic_token, DOM_XMLNS_NS_URI); static void php_dom_libxml_ns_mapper_prefix_map_element_dtor(zval *zv) { @@ -69,27 +71,6 @@ static HashTable *php_dom_libxml_ns_mapper_ensure_prefix_map(php_dom_libxml_ns_m return prefix_map; } -static void php_dom_libxml_ns_mapper_header_destroy(php_libxml_private_data_header *header) -{ - php_dom_libxml_ns_mapper_destroy((php_dom_libxml_ns_mapper *) header); -} - -PHP_DOM_EXPORT php_dom_libxml_ns_mapper *php_dom_libxml_ns_mapper_create(void) -{ - php_dom_libxml_ns_mapper *mapper = emalloc(sizeof(*mapper)); - mapper->header.dtor = php_dom_libxml_ns_mapper_header_destroy; - mapper->html_ns = NULL; - mapper->prefixless_xmlns_ns = NULL; - zend_hash_init(&mapper->uri_to_prefix_map, 0, NULL, ZVAL_PTR_DTOR, false); - return mapper; -} - -void php_dom_libxml_ns_mapper_destroy(php_dom_libxml_ns_mapper *mapper) -{ - zend_hash_destroy(&mapper->uri_to_prefix_map); - efree(mapper); -} - static xmlNsPtr php_dom_libxml_ns_mapper_ensure_cached_ns(php_dom_libxml_ns_mapper *mapper, xmlNsPtr *ptr, const char *uri, size_t length, const php_dom_ns_magic_token *token) { if (EXPECTED(*ptr != NULL)) { @@ -229,11 +210,6 @@ static xmlNsPtr php_dom_libxml_ns_mapper_store_and_normalize_parsed_ns(php_dom_l return ns; } -PHP_DOM_EXPORT php_libxml_private_data_header *php_dom_libxml_ns_mapper_header(php_dom_libxml_ns_mapper *mapper) -{ - return mapper == NULL ? NULL : &mapper->header; -} - typedef struct { /* Fast lookup for created mappings. */ HashTable old_ns_to_new_ns_ptr; @@ -243,6 +219,11 @@ typedef struct { php_dom_libxml_ns_mapper *ns_mapper; } dom_libxml_reconcile_ctx; +PHP_DOM_EXPORT php_dom_libxml_ns_mapper *php_dom_get_ns_mapper(dom_object *object) +{ + return &php_dom_get_private_data(object)->ns_mapper; +} + PHP_DOM_EXPORT xmlAttrPtr php_dom_ns_compat_mark_attribute(php_dom_libxml_ns_mapper *mapper, xmlNodePtr node, xmlNsPtr ns) { xmlNsPtr xmlns_ns; @@ -302,13 +283,16 @@ PHP_DOM_EXPORT bool php_dom_ns_is_fast_ex(xmlNsPtr ns, const php_dom_ns_magic_to /* cached for fast checking */ if (ns->_private == magic_token) { return true; - } else if (ns->_private != NULL) { + } else if (ns->_private != NULL && ((uintptr_t) ns->_private & 1) == 0) { /* Other token stored */ return false; } /* Slow path */ if (xmlStrEqual(ns->href, BAD_CAST magic_token)) { - ns->_private = (void *) magic_token; + if (ns->_private == NULL) { + /* Only overwrite the private data if there is no other token stored. */ + ns->_private = (void *) magic_token; + } return true; } return false; @@ -352,14 +336,6 @@ PHP_DOM_EXPORT void php_dom_reconcile_attribute_namespace_after_insertion(xmlAtt } } -static zend_always_inline zend_long dom_mangle_pointer_for_key(void *ptr) -{ - zend_ulong value = (zend_ulong) (uintptr_t) ptr; - /* Rotate 3/4 bits for better hash distribution because the low 3/4 bits are normally 0. */ - const size_t rol_amount = (SIZEOF_ZEND_LONG == 8) ? 4 : 3; - return (value >> rol_amount) | (value << (sizeof(value) * 8 - rol_amount)); -} - static zend_always_inline void php_dom_libxml_reconcile_modern_single_node(dom_libxml_reconcile_ctx *ctx, xmlNodePtr node) { ZEND_ASSERT(node->ns != NULL); diff --git a/ext/dom/namespace_compat.h b/ext/dom/namespace_compat.h index bf0a295e1bee6..23c80acc7fd78 100644 --- a/ext/dom/namespace_compat.h +++ b/ext/dom/namespace_compat.h @@ -40,21 +40,16 @@ PHP_DOM_EXPORT extern const php_dom_ns_magic_token *php_dom_ns_is_xlink_magic_to PHP_DOM_EXPORT extern const php_dom_ns_magic_token *php_dom_ns_is_xml_magic_token; PHP_DOM_EXPORT extern const php_dom_ns_magic_token *php_dom_ns_is_xmlns_magic_token; -typedef struct php_libxml_private_data_header php_libxml_private_data_header; -struct php_libxml_private_data_header; - /* These functions make it possible to make a namespace declaration also visible as an attribute by * creating an equivalent attribute node. */ -PHP_DOM_EXPORT php_dom_libxml_ns_mapper *php_dom_libxml_ns_mapper_create(void); -PHP_DOM_EXPORT void php_dom_libxml_ns_mapper_destroy(php_dom_libxml_ns_mapper *mapper); PHP_DOM_EXPORT xmlNsPtr php_dom_libxml_ns_mapper_ensure_html_ns(php_dom_libxml_ns_mapper *mapper); PHP_DOM_EXPORT xmlNsPtr php_dom_libxml_ns_mapper_ensure_prefixless_xmlns_ns(php_dom_libxml_ns_mapper *mapper); PHP_DOM_EXPORT xmlNsPtr php_dom_libxml_ns_mapper_get_ns(php_dom_libxml_ns_mapper *mapper, zend_string *prefix, zend_string *uri); PHP_DOM_EXPORT xmlNsPtr php_dom_libxml_ns_mapper_get_ns_raw_prefix_string(php_dom_libxml_ns_mapper *mapper, const xmlChar *prefix, size_t prefix_len, zend_string *uri); PHP_DOM_EXPORT xmlNsPtr php_dom_libxml_ns_mapper_get_ns_raw_strings_nullsafe(php_dom_libxml_ns_mapper *mapper, const char *prefix, const char *uri); -PHP_DOM_EXPORT php_libxml_private_data_header *php_dom_libxml_ns_mapper_header(php_dom_libxml_ns_mapper *mapper); +PHP_DOM_EXPORT php_dom_libxml_ns_mapper *php_dom_get_ns_mapper(dom_object *object); PHP_DOM_EXPORT void php_dom_ns_compat_mark_attribute_list(php_dom_libxml_ns_mapper *mapper, xmlNodePtr node); PHP_DOM_EXPORT void php_dom_libxml_reconcile_modern(php_dom_libxml_ns_mapper *ns_mapper, xmlNodePtr node); PHP_DOM_EXPORT void php_dom_reconcile_attribute_namespace_after_insertion(xmlAttrPtr attrp); diff --git a/ext/dom/node.c b/ext/dom/node.c index ef669963de78b..971e7f4d9b5cd 100644 --- a/ext/dom/node.c +++ b/ext/dom/node.c @@ -23,6 +23,7 @@ #if defined(HAVE_LIBXML) && defined(HAVE_DOM) #include "php_dom.h" #include "namespace_compat.h" +#include "private_data.h" #include "internal_helpers.h" #include "dom_properties.h" @@ -1444,21 +1445,21 @@ PHP_METHOD(DOMNode, cloneNode) DOM_GET_OBJ(n, id, xmlNodePtr, intern); - php_dom_libxml_ns_mapper *ns_mapper = NULL; + php_dom_private_data *private_data = NULL; bool clone_document = n->type == XML_DOCUMENT_NODE || n->type == XML_HTML_DOCUMENT_NODE; if (php_dom_follow_spec_intern(intern)) { if (clone_document) { - ns_mapper = php_dom_libxml_ns_mapper_create(); + private_data = php_dom_private_data_create(); } else { - ns_mapper = php_dom_get_ns_mapper(intern); + private_data = php_dom_get_private_data(intern); } } - node = dom_clone_node(ns_mapper, n, n->doc, recursive); + node = dom_clone_node(php_dom_ns_mapper_from_private(private_data), n, n->doc, recursive); if (!node) { - if (clone_document && ns_mapper != NULL) { - php_dom_libxml_ns_mapper_destroy(ns_mapper); + if (clone_document && private_data != NULL) { + php_dom_private_data_destroy(private_data); } RETURN_FALSE; } @@ -1466,7 +1467,7 @@ PHP_METHOD(DOMNode, cloneNode) /* If document cloned we want a new document proxy */ if (clone_document) { dom_object *new_intern; - if (ns_mapper) { + if (private_data) { /* We have the issue here that we can't create a modern node without an intern. * Fortunately, it's impossible to have a custom document class for the modern DOM (final base class), * so we can solve this by invoking the instantiation helper directly. */ @@ -1478,7 +1479,7 @@ PHP_METHOD(DOMNode, cloneNode) } php_dom_update_document_after_clone(intern, n, new_intern, node); ZEND_ASSERT(new_intern->document->private_data == NULL); - new_intern->document->private_data = php_dom_libxml_ns_mapper_header(ns_mapper); + new_intern->document->private_data = php_dom_libxml_private_data_header(private_data); } else { if (node->type == XML_ATTRIBUTE_NODE && n->ns != NULL && node->ns == NULL) { /* Let reconciliation deal with this. The lifetime of the namespace poses no problem diff --git a/ext/dom/php_dom.c b/ext/dom/php_dom.c index e6e7a23600d94..ce7653198368d 100644 --- a/ext/dom/php_dom.c +++ b/ext/dom/php_dom.c @@ -27,6 +27,7 @@ #include "nodelist.h" #include "html_collection.h" #include "namespace_compat.h" +#include "private_data.h" #include "internal_helpers.h" #include "php_dom_arginfo.h" #include "dom_properties.h" @@ -593,21 +594,21 @@ static zend_object *dom_objects_store_clone_obj(zend_object *zobject) /* {{{ */ if (instanceof_function(intern->std.ce, dom_node_class_entry) || instanceof_function(intern->std.ce, dom_modern_node_class_entry)) { xmlNodePtr node = (xmlNodePtr)dom_object_get_node(intern); if (node != NULL) { - php_dom_libxml_ns_mapper *ns_mapper = NULL; + php_dom_private_data *private_data = NULL; if (php_dom_follow_spec_intern(intern)) { if (node->type == XML_DOCUMENT_NODE || node->type == XML_HTML_DOCUMENT_NODE) { - ns_mapper = php_dom_libxml_ns_mapper_create(); + private_data = php_dom_private_data_create(); } else { - ns_mapper = php_dom_get_ns_mapper(intern); + private_data = php_dom_get_private_data(intern); } } - xmlNodePtr cloned_node = dom_clone_node(ns_mapper, node, node->doc, true); + xmlNodePtr cloned_node = dom_clone_node(php_dom_ns_mapper_from_private(private_data), node, node->doc, true); if (cloned_node != NULL) { dom_update_refcount_after_clone(intern, node, clone, cloned_node); } - if (ns_mapper != NULL) { - clone->document->private_data = php_dom_libxml_ns_mapper_header(ns_mapper); + if (private_data != NULL) { + clone->document->private_data = php_dom_libxml_private_data_header(private_data); } } } @@ -1389,8 +1390,11 @@ void dom_objects_free_storage(zend_object *object) zend_object_std_dtor(&intern->std); - if (intern->ptr != NULL && ((php_libxml_node_ptr *)intern->ptr)->node != NULL) { - if (((xmlNodePtr) ((php_libxml_node_ptr *)intern->ptr)->node)->type != XML_DOCUMENT_NODE && ((xmlNodePtr) ((php_libxml_node_ptr *)intern->ptr)->node)->type != XML_HTML_DOCUMENT_NODE) { + php_libxml_node_ptr *ptr = intern->ptr; + if (ptr != NULL && ptr->node != NULL) { + xmlNodePtr node = ptr->node; + + if (node->type != XML_DOCUMENT_NODE && node->type != XML_HTML_DOCUMENT_NODE) { php_libxml_node_decrement_resource((php_libxml_node_object *) intern); } else { php_libxml_decrement_node_ptr((php_libxml_node_object *) intern); diff --git a/ext/dom/php_dom.h b/ext/dom/php_dom.h index aa1f3316c206a..624e3cbaa5760 100644 --- a/ext/dom/php_dom.h +++ b/ext/dom/php_dom.h @@ -114,6 +114,9 @@ typedef enum dom_iterator_type { DOM_HTMLCOLLECTION, } dom_iterator_type; +struct php_dom_libxml_ns_mapper; +typedef struct php_dom_libxml_ns_mapper php_dom_libxml_ns_mapper; + static inline dom_object_namespace_node *php_dom_namespace_node_obj_from_obj(zend_object *obj) { return (dom_object_namespace_node*)((char*)(obj) - XtOffsetOf(dom_object_namespace_node, dom.std)); } diff --git a/ext/dom/php_dom.stub.php b/ext/dom/php_dom.stub.php index 15d639fb27e94..f678c7376d13e 100644 --- a/ext/dom/php_dom.stub.php +++ b/ext/dom/php_dom.stub.php @@ -1643,6 +1643,10 @@ public function saveXmlFile(string $filename, int $options = 0): int|false {} public function saveHtml(?Node $node = null): string {} public function saveHtmlFile(string $filename): int|false {} + +#if ZEND_DEBUG + public function debugGetTemplateCount(): int {} +#endif } final class XMLDocument extends Document diff --git a/ext/dom/php_dom_arginfo.h b/ext/dom/php_dom_arginfo.h index fe42290b5f01a..90db1e81af061 100644 --- a/ext/dom/php_dom_arginfo.h +++ b/ext/dom/php_dom_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 1af73c3b63ebeb5e59948990892dcf6b627a1671 */ + * Stub hash: 9a1e6842b2c5b891e11087d40aa8c9f56a2269a3 */ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_dom_import_simplexml, 0, 1, DOMElement, 0) ZEND_ARG_TYPE_INFO(0, node, IS_OBJECT, 0) @@ -1059,6 +1059,11 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_class_Dom_HTMLDocument_saveHtmlF ZEND_ARG_TYPE_INFO(0, filename, IS_STRING, 0) ZEND_END_ARG_INFO() +#if ZEND_DEBUG +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Dom_HTMLDocument_debugGetTemplateCount, 0, 0, IS_LONG, 0) +ZEND_END_ARG_INFO() +#endif + ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Dom_XMLDocument_createEmpty, 0, 0, Dom\\XMLDocument, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, version, IS_STRING, 0, "\"1.0\"") ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, encoding, IS_STRING, 0, "\"UTF-8\"") @@ -1367,6 +1372,9 @@ ZEND_METHOD(Dom_HTMLDocument, createFromString); ZEND_METHOD(Dom_XMLDocument, saveXml); ZEND_METHOD(Dom_HTMLDocument, saveHtml); ZEND_METHOD(Dom_HTMLDocument, saveHtmlFile); +#if ZEND_DEBUG +ZEND_METHOD(Dom_HTMLDocument, debugGetTemplateCount); +#endif ZEND_METHOD(Dom_XMLDocument, createEmpty); ZEND_METHOD(Dom_XMLDocument, createFromFile); ZEND_METHOD(Dom_XMLDocument, createFromString); @@ -1885,6 +1893,9 @@ static const zend_function_entry class_Dom_HTMLDocument_methods[] = { ZEND_RAW_FENTRY("saveXmlFile", zim_DOMDocument_save, arginfo_class_Dom_HTMLDocument_saveXmlFile, ZEND_ACC_PUBLIC, NULL, NULL) ZEND_ME(Dom_HTMLDocument, saveHtml, arginfo_class_Dom_HTMLDocument_saveHtml, ZEND_ACC_PUBLIC) ZEND_ME(Dom_HTMLDocument, saveHtmlFile, arginfo_class_Dom_HTMLDocument_saveHtmlFile, ZEND_ACC_PUBLIC) +#if ZEND_DEBUG + ZEND_ME(Dom_HTMLDocument, debugGetTemplateCount, arginfo_class_Dom_HTMLDocument_debugGetTemplateCount, ZEND_ACC_PUBLIC) +#endif ZEND_FE_END }; diff --git a/ext/dom/private_data.c b/ext/dom/private_data.c new file mode 100644 index 0000000000000..bb20093b8ebbf --- /dev/null +++ b/ext/dom/private_data.c @@ -0,0 +1,169 @@ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Authors: Niels Dossche | + +----------------------------------------------------------------------+ +*/ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "php.h" +#if defined(HAVE_LIBXML) && defined(HAVE_DOM) +#include "php_dom.h" +#include "private_data.h" +#include "internal_helpers.h" + +static void php_dom_libxml_private_data_destroy(php_libxml_private_data_header *header) +{ + php_dom_private_data_destroy((php_dom_private_data *) header); +} + +static void php_dom_libxml_private_data_ns_hook(php_libxml_private_data_header *header, xmlNodePtr node) +{ + php_dom_remove_templated_content((php_dom_private_data *) header, node); +} + +php_libxml_private_data_header *php_dom_libxml_private_data_header(php_dom_private_data *private_data) +{ + return private_data == NULL ? NULL : &private_data->header; +} + +php_dom_libxml_ns_mapper *php_dom_ns_mapper_from_private(php_dom_private_data *private_data) +{ + return private_data == NULL ? NULL : &private_data->ns_mapper; +} + +php_dom_private_data *php_dom_private_data_create(void) +{ + php_dom_private_data *private_data = emalloc(sizeof(*private_data)); + private_data->header.dtor = php_dom_libxml_private_data_destroy; + private_data->header.ns_hook = php_dom_libxml_private_data_ns_hook; + private_data->ns_mapper.html_ns = NULL; + private_data->ns_mapper.prefixless_xmlns_ns = NULL; + zend_hash_init(&private_data->ns_mapper.uri_to_prefix_map, 0, NULL, ZVAL_PTR_DTOR, false); + private_data->template_fragments = NULL; + return private_data; +} + +void php_dom_private_data_destroy(php_dom_private_data *data) +{ + zend_hash_destroy(&data->ns_mapper.uri_to_prefix_map); + if (data->template_fragments != NULL) { + xmlNodePtr node; + ZEND_HASH_MAP_FOREACH_PTR(data->template_fragments, node) { + xmlFreeNode(node); + } ZEND_HASH_FOREACH_END(); + zend_hash_destroy(data->template_fragments); + FREE_HASHTABLE(data->template_fragments); + } + efree(data); +} + +static void php_dom_free_templated_content(php_dom_private_data *private_data, xmlNodePtr base) +{ + /* Note: it's not possible to obtain a userland reference to these yet, so we can just free them without worrying + * about their proxies. + * Note 2: it's possible to have nested template content. */ + + if (zend_hash_num_elements(private_data->template_fragments) > 0) { + /* There's more templated content, try to free it. */ + xmlNodePtr current = base->children; + while (current != NULL) { + if (current->type == XML_ELEMENT_NODE) { + php_dom_remove_templated_content(private_data, current); + } + + current = php_dom_next_in_tree_order(current, base); + } + } + + xmlFreeNode(base); +} + +void php_dom_add_templated_content(php_dom_private_data *private_data, const xmlNode *template_node, xmlNodePtr fragment) +{ + if (private_data->template_fragments == NULL) { + ALLOC_HASHTABLE(private_data->template_fragments); + zend_hash_init(private_data->template_fragments, 0, NULL, NULL, false); + zend_hash_real_init_mixed(private_data->template_fragments); + } + + zend_hash_index_add_new_ptr(private_data->template_fragments, dom_mangle_pointer_for_key(template_node), fragment); +} + +xmlNodePtr php_dom_retrieve_templated_content(php_dom_private_data *private_data, const xmlNode *template_node) +{ + if (private_data->template_fragments == NULL) { + return NULL; + } + + return zend_hash_index_find_ptr(private_data->template_fragments, dom_mangle_pointer_for_key(template_node)); +} + +xmlNodePtr php_dom_ensure_templated_content(php_dom_private_data *private_data, xmlNodePtr template_node) +{ + xmlNodePtr result = php_dom_retrieve_templated_content(private_data, template_node); + if (result == NULL) { + result = xmlNewDocFragment(template_node->doc); + if (EXPECTED(result != NULL)) { + result->parent = template_node; + dom_add_element_ns_hook(private_data, template_node); + php_dom_add_templated_content(private_data, template_node, result); + } + } + return result; +} + +void php_dom_remove_templated_content(php_dom_private_data *private_data, const xmlNode *template_node) +{ + if (private_data->template_fragments != NULL) { + /* Deletion needs to be done not via a destructor because we can't access private_data from there. */ + zval *zv = zend_hash_index_find(private_data->template_fragments, dom_mangle_pointer_for_key(template_node)); + if (zv != NULL) { + xmlNodePtr node = Z_PTR_P(zv); + ZEND_ASSERT(offsetof(Bucket, val) == 0 && "Type cast only works if this is true"); + Bucket* bucket = (Bucket*) zv; + /* First remove it from the bucket before freeing the content, otherwise recursion could make the bucket + * pointer invalid due to hash table structure changes. */ + zend_hash_del_bucket(private_data->template_fragments, bucket); + php_dom_free_templated_content(private_data, node); + } + } +} + +uint32_t php_dom_get_template_count(const php_dom_private_data *private_data) +{ + if (private_data->template_fragments != NULL) { + return zend_hash_num_elements(private_data->template_fragments); + } else { + return 0; + } +} + +void dom_add_element_ns_hook(php_dom_private_data *private_data, xmlNodePtr element) +{ + xmlNsPtr ns = pemalloc(sizeof(*ns), true); + + /* The private data is a tagged data structure where only tag 1 is defined by ext/libxml to register a hook. */ + memset(ns, 0, sizeof(*ns)); + ns->prefix = xmlStrdup(element->ns->prefix); + ns->href = xmlStrdup(element->ns->href); + ns->type = XML_LOCAL_NAMESPACE; + ns->_private = (void *) ((uintptr_t) private_data | LIBXML_NS_TAG_HOOK); + element->ns = ns; + + php_libxml_set_old_ns(element->doc, ns); +} + +#endif /* HAVE_LIBXML && HAVE_DOM */ diff --git a/ext/dom/private_data.h b/ext/dom/private_data.h new file mode 100644 index 0000000000000..ead6c75caf249 --- /dev/null +++ b/ext/dom/private_data.h @@ -0,0 +1,56 @@ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Authors: Niels Dossche | + +----------------------------------------------------------------------+ +*/ + +#ifndef PRIVATE_DATA_H +#define PRIVATE_DATA_H + +#include "xml_common.h" + +struct php_dom_libxml_ns_mapper { + /* This is used almost all the time for HTML documents, so it makes sense to cache this. */ + xmlNsPtr html_ns; + /* Used for every prefixless namespace declaration in XML, so also very common. */ + xmlNsPtr prefixless_xmlns_ns; + HashTable uri_to_prefix_map; +}; + +typedef struct php_dom_private_data { + php_libxml_private_data_header header; + struct php_dom_libxml_ns_mapper ns_mapper; + HashTable *template_fragments; +} php_dom_private_data; + +typedef struct php_libxml_private_data_header php_libxml_private_data_header; +struct php_libxml_private_data_header; + +struct php_dom_private_data; +typedef struct php_dom_private_data php_dom_private_data; + +struct php_dom_libxml_ns_mapper; +typedef struct php_dom_libxml_ns_mapper php_dom_libxml_ns_mapper; + +php_libxml_private_data_header *php_dom_libxml_private_data_header(php_dom_private_data *private_data); +php_dom_libxml_ns_mapper *php_dom_ns_mapper_from_private(php_dom_private_data *private_data); +php_dom_private_data *php_dom_private_data_create(void); +void php_dom_private_data_destroy(php_dom_private_data *data); +void php_dom_add_templated_content(php_dom_private_data *private_data, const xmlNode *template_node, xmlNodePtr fragment); +xmlNodePtr php_dom_retrieve_templated_content(php_dom_private_data *private_data, const xmlNode *template_node); +xmlNodePtr php_dom_ensure_templated_content(php_dom_private_data *private_data, xmlNodePtr template_node); +void php_dom_remove_templated_content(php_dom_private_data *private_data, const xmlNode *template_node); +uint32_t php_dom_get_template_count(const php_dom_private_data *private_data); +void dom_add_element_ns_hook(php_dom_private_data *private_data, xmlNodePtr element); + +#endif diff --git a/ext/dom/tests/modern/common/template_cloning.phpt b/ext/dom/tests/modern/common/template_cloning.phpt new file mode 100644 index 0000000000000..3ba9c8276495a --- /dev/null +++ b/ext/dom/tests/modern/common/template_cloning.phpt @@ -0,0 +1,14 @@ +--TEST-- +Template cloning +--EXTENSIONS-- +dom +--FILE-- +x', LIBXML_NOERROR); +$a = $dom->head->firstChild->cloneNode(false); +echo $dom->saveXML($a), "\n"; +echo $dom->saveHTML($a), "\n"; +?> +--EXPECT-- + + diff --git a/ext/dom/tests/modern/common/template_indirect_removal.phpt b/ext/dom/tests/modern/common/template_indirect_removal.phpt new file mode 100644 index 0000000000000..b257464e0f2f3 --- /dev/null +++ b/ext/dom/tests/modern/common/template_indirect_removal.phpt @@ -0,0 +1,22 @@ +--TEST-- +template content indirect removal +--EXTENSIONS-- +dom +--SKIPIF-- + +--FILE-- +foo', LIBXML_NOERROR); +$head = $dom->head; +var_dump($dom->debugGetTemplateCount()); +$head->remove(); +var_dump($dom->debugGetTemplateCount()); +unset($head); +var_dump($dom->debugGetTemplateCount()); +?> +--EXPECT-- +int(2) +int(2) +int(0) diff --git a/ext/dom/tests/modern/common/template_manual.phpt b/ext/dom/tests/modern/common/template_manual.phpt new file mode 100644 index 0000000000000..f350517468ae3 --- /dev/null +++ b/ext/dom/tests/modern/common/template_manual.phpt @@ -0,0 +1,36 @@ +--TEST-- +

"; +var_dump($template->innerHTML); +var_dump($template->firstChild); +echo $dom->saveXML(), "\n"; +echo $dom->saveHTML(), "\n"; + +?> +--EXPECT-- +=== After creation === +string(0) "" + + + +=== After setting content === +string(12) "

hello

" +NULL + + + diff --git a/ext/dom/tests/modern/common/template_nested.phpt b/ext/dom/tests/modern/common/template_nested.phpt new file mode 100644 index 0000000000000..1b991368607d9 --- /dev/null +++ b/ext/dom/tests/modern/common/template_nested.phpt @@ -0,0 +1,29 @@ +--TEST-- +