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--
+foonested', 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--
+ element manual creation
+--EXTENSIONS--
+dom
+--FILE--
+appendChild($dom->createElement("template"));
+var_dump($template->innerHTML);
+echo $dom->saveXML(), "\n";
+echo $dom->saveHTML(), "\n";
+
+echo "=== After setting content ===\n";
+
+$template->innerHTML = "hello
";
+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
+
+hello
+hello
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--
+ element nesting
+--EXTENSIONS--
+dom
+--FILE--
+
+
+
+ foobar
+
+
+HTML;
+$dom = Dom\HTMLDocument::createFromString($html);
+$template = $dom->body->firstElementChild;
+var_dump($template->innerHTML);
+echo $dom->saveXML();
+
+?>
+--EXPECT--
+string(27) "foobar"
+
+
+
+ foobar
+
+
diff --git a/ext/dom/tests/modern/common/template_no_default_ns.phpt b/ext/dom/tests/modern/common/template_no_default_ns.phpt
new file mode 100644
index 0000000000000..fc3cd163044f8
--- /dev/null
+++ b/ext/dom/tests/modern/common/template_no_default_ns.phpt
@@ -0,0 +1,24 @@
+--TEST--
+ element no default namespace
+--EXTENSIONS--
+dom
+--FILE--
+
+
+
+ afoo
b
+
+
+HTML;
+$dom = Dom\HTMLDocument::createFromString($html, Dom\HTML_NO_DEFAULT_NS);
+$template = $dom->getElementsByTagName('template')[0];
+var_dump($template->innerHTML);
+var_dump($template->firstElementChild->tagName);
+
+?>
+--EXPECT--
+string(16) "afoo
b"
+string(3) "div"
diff --git a/ext/dom/tests/modern/common/template_participation.phpt b/ext/dom/tests/modern/common/template_participation.phpt
new file mode 100644
index 0000000000000..374a56102b2ee
--- /dev/null
+++ b/ext/dom/tests/modern/common/template_participation.phpt
@@ -0,0 +1,83 @@
+--TEST--
+ element contents do not participate in DOM
+--EXTENSIONS--
+dom
+--FILE--
+
+
+
+ afoo
b
+
+
+HTML;
+$dom = Dom\HTMLDocument::createFromString($html);
+$template = $dom->body->firstElementChild;
+
+echo "=== Manipulation ===\n";
+
+echo "First child of template: ";
+var_dump($template->firstChild?->nodeName);
+$template->append($dom->createElement('invisible'));
+
+echo "First child of template after appending: ";
+var_dump($template->firstChild->nodeName);
+$template->innerHTML = $template->innerHTML;
+echo "Inner HTML after idempotent modification: ";
+var_dump($template->innerHTML);
+echo "Selector should not find div element in shadow DOM: ";
+var_dump($template->querySelector('div'));
+
+echo "XPath should not find div element in shadow DOM:\n";
+$xpath = new Dom\XPath($dom);
+var_dump($xpath->query('//div'));
+
+echo "=== HTML serialization ===\n";
+echo $dom->saveHTML(), "\n";
+echo "=== HTML serialization of ===\n";
+echo $dom->saveHTML($template), "\n";
+echo "=== XML serialization ===\n";
+echo $dom->saveXML(), "\n";
+echo "=== XML serialization of ===\n";
+echo $dom->saveXML($template), "\n";
+
+// Should not crash
+$template->remove();
+unset($template);
+
+echo "=== Creating a new template should not leak the old contents ===\n";
+$template = $dom->createElement('template');
+var_dump($template->innerHTML);
+
+?>
+--EXPECT--
+=== Manipulation ===
+First child of template: NULL
+First child of template after appending: string(9) "INVISIBLE"
+Inner HTML after idempotent modification: string(16) "afoo
b"
+Selector should not find div element in shadow DOM: NULL
+XPath should not find div element in shadow DOM:
+object(Dom\NodeList)#4 (1) {
+ ["length"]=>
+ int(0)
+}
+=== HTML serialization ===
+
+ afoo
b
+
+
+=== HTML serialization of ===
+afoo
b
+=== XML serialization ===
+
+
+
+ afoo
b
+
+
+=== XML serialization of ===
+afoo
b
+=== Creating a new template should not leak the old contents ===
+string(0) ""
diff --git a/ext/dom/tests/modern/common/template_rename.phpt b/ext/dom/tests/modern/common/template_rename.phpt
new file mode 100644
index 0000000000000..a71126b21f22a
--- /dev/null
+++ b/ext/dom/tests/modern/common/template_rename.phpt
@@ -0,0 +1,35 @@
+--TEST--
+ element renaming
+--EXTENSIONS--
+dom
+--FILE--
+
+
+
+ afoo
b
+
+
+HTML;
+$dom = Dom\HTMLDocument::createFromString($html);
+$template = $dom->body->firstElementChild;
+var_dump($template->innerHTML);
+
+try {
+ $template->rename($template->namespaceURI, 'screwthis');
+} catch (DOMException $e) {
+ echo $e->getMessage(), "\n";
+}
+
+// These shouldn't be changed!
+var_dump($template->nodeName);
+var_dump($template->innerHTML);
+
+?>
+--EXPECT--
+string(16) "afoo
b"
+It is not possible to rename the template element because it hosts a document fragment
+string(8) "TEMPLATE"
+string(16) "afoo
b"
diff --git a/ext/dom/tests/modern/common/template_simplexml.phpt b/ext/dom/tests/modern/common/template_simplexml.phpt
new file mode 100644
index 0000000000000..52e6aee7d0f10
--- /dev/null
+++ b/ext/dom/tests/modern/common/template_simplexml.phpt
@@ -0,0 +1,30 @@
+--TEST--
+SimpleXML and template content
+--EXTENSIONS--
+dom
+simplexml
+--SKIPIF--
+
+--FILE--
+foonested', LIBXML_NOERROR);
+$head = $dom->head;
+$head_sxe = simplexml_import_dom($head);
+var_dump($head_sxe);
+var_dump($dom->debugGetTemplateCount());
+unset($head_sxe->template);
+var_dump($head_sxe);
+var_dump($dom->debugGetTemplateCount());
+?>
+--EXPECTF--
+object(SimpleXMLElement)#%d (1) {
+ ["template"]=>
+ object(SimpleXMLElement)#%d (0) {
+ }
+}
+int(2)
+object(SimpleXMLElement)#%d (0) {
+}
+int(0)
diff --git a/ext/dom/xml_common.h b/ext/dom/xml_common.h
index 94c753132b893..8e5734d5a914e 100644
--- a/ext/dom/xml_common.h
+++ b/ext/dom/xml_common.h
@@ -80,13 +80,13 @@ PHP_DOM_EXPORT xmlNodePtr dom_object_get_node(dom_object *obj);
__id = ZEND_THIS; \
DOM_GET_OBJ(__ptr, __id, __prtype, __intern);
-struct php_dom_libxml_ns_mapper;
-typedef struct php_dom_libxml_ns_mapper php_dom_libxml_ns_mapper;
+struct php_dom_private_data;
+typedef struct php_dom_private_data php_dom_private_data;
-static zend_always_inline php_dom_libxml_ns_mapper *php_dom_get_ns_mapper(dom_object *intern)
+static zend_always_inline php_dom_private_data *php_dom_get_private_data(dom_object *intern)
{
ZEND_ASSERT(intern->document != NULL);
- return (php_dom_libxml_ns_mapper *) intern->document->private_data;
+ return (php_dom_private_data *) intern->document->private_data;
}
static zend_always_inline xmlNodePtr php_dom_next_in_tree_order(const xmlNode *nodep, const xmlNode *basep)
diff --git a/ext/dom/xml_document.c b/ext/dom/xml_document.c
index e1511b65ef798..2bd3d908d7093 100644
--- a/ext/dom/xml_document.c
+++ b/ext/dom/xml_document.c
@@ -22,6 +22,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
@@ -122,7 +123,7 @@ PHP_METHOD(Dom_XMLDocument, 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:
@@ -236,8 +237,9 @@ static void load_from_helper(INTERNAL_FUNCTION_PARAMETERS, int mode)
void dom_document_convert_to_modern(php_libxml_ref_obj *document, xmlDocPtr lxml_doc)
{
- php_dom_libxml_ns_mapper *ns_mapper = php_dom_libxml_ns_mapper_create();
- document->private_data = php_dom_libxml_ns_mapper_header(ns_mapper);
+ 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);
+ document->private_data = php_dom_libxml_private_data_header(private_data);
dom_mark_namespaces_as_attributes_too(ns_mapper, lxml_doc);
}
@@ -258,6 +260,12 @@ static int php_new_dom_write_smart_str(void *context, const char *buffer, int le
return len;
}
+static php_dom_private_data *get_private_data_from_node(xmlNodePtr node)
+{
+ dom_object *intern = php_dom_object_get_data(node);
+ return intern != NULL ? php_dom_get_private_data(intern) : NULL;
+}
+
static zend_string *php_new_dom_dump_node_to_str_ex(xmlNodePtr node, int options, bool format, const char *encoding)
{
smart_str str = {0};
@@ -268,7 +276,7 @@ static zend_string *php_new_dom_dump_node_to_str_ex(xmlNodePtr node, int options
xmlCharEncodingHandlerPtr handler = xmlFindCharEncodingHandler(encoding);
xmlOutputBufferPtr out = xmlOutputBufferCreateIO(php_new_dom_write_smart_str, NULL, &str, handler);
if (EXPECTED(out != NULL)) {
- status = dom_xml_serialize(ctxt, out, node, format, false);
+ status = dom_xml_serialize(ctxt, out, node, format, false, get_private_data_from_node(node));
status |= xmlOutputBufferFlush(out);
status |= xmlOutputBufferClose(out);
} else {
@@ -309,7 +317,7 @@ zend_long php_new_dom_dump_node_to_file(const char *filename, xmlDocPtr doc, xml
int status = -1;
xmlSaveCtxtPtr ctxt = xmlSaveToIO(out->writecallback, NULL, stream, encoding, XML_SAVE_AS_XML);
if (EXPECTED(ctxt != NULL)) {
- status = dom_xml_serialize(ctxt, out, node, format, false);
+ status = dom_xml_serialize(ctxt, out, node, format, false, get_private_data_from_node(node));
status |= xmlOutputBufferFlush(out);
(void) xmlSaveClose(ctxt);
}
diff --git a/ext/dom/xml_serializer.c b/ext/dom/xml_serializer.c
index 0c5f12023aa6d..080f7ed30aa64 100644
--- a/ext/dom/xml_serializer.c
+++ b/ext/dom/xml_serializer.c
@@ -21,6 +21,7 @@
#include "php.h"
#if defined(HAVE_LIBXML) && defined(HAVE_DOM)
#include "xml_serializer.h"
+#include "private_data.h"
#include "namespace_compat.h"
#include "serialize_common.h"
#include "internal_helpers.h"
@@ -66,9 +67,14 @@ typedef struct {
const xmlChar *prefix, *name;
} dom_qname_pair;
+typedef struct dom_xml_serialize_ctx {
+ xmlSaveCtxtPtr ctxt;
+ xmlOutputBufferPtr out;
+ php_dom_private_data *private_data;
+} dom_xml_serialize_ctx;
+
static int dom_xml_serialization_algorithm(
- xmlSaveCtxtPtr ctxt,
- xmlOutputBufferPtr out,
+ dom_xml_serialize_ctx *ctx,
dom_xml_ns_prefix_map *namespace_prefix_map,
xmlNodePtr node,
const xmlChar *namespace,
@@ -895,8 +901,7 @@ static int dom_xml_output_indents(xmlOutputBufferPtr out, int indent)
/* https://w3c.github.io/DOM-Parsing/#dfn-xml-serializing-an-element-node */
static int dom_xml_serialize_element_node(
- xmlSaveCtxtPtr ctxt,
- xmlOutputBufferPtr out,
+ dom_xml_serialize_ctx *ctx,
const xmlChar *namespace,
dom_xml_ns_prefix_map *namespace_prefix_map,
xmlNodePtr element,
@@ -916,7 +921,7 @@ static int dom_xml_serialize_element_node(
bool should_format = indent >= 0 && element->children != NULL && dom_xml_should_format_element(element);
/* 2. Let markup be the string "<" (U+003C LESS-THAN SIGN). */
- TRY(xmlOutputBufferWriteLit(out, "<"));
+ TRY(xmlOutputBufferWriteLit(ctx->out, "<"));
/* 3. Let qualified name be an empty string.
* => We're going to do it a bit differently.
@@ -966,7 +971,7 @@ static int dom_xml_serialize_element_node(
}
/* 11.4. Append the value of qualified name to markup. */
- TRY_OR_CLEANUP(dom_xml_output_qname(out, &qualified_name));
+ TRY_OR_CLEANUP(dom_xml_output_qname(ctx->out, &qualified_name));
}
/* 12. Otherwise, inherited ns is not equal to ns */
else {
@@ -1011,7 +1016,7 @@ static int dom_xml_serialize_element_node(
}
/* 12.4.3. Append the value of qualified name to markup. */
- TRY_OR_CLEANUP(dom_xml_output_qname(out, &qualified_name));
+ TRY_OR_CLEANUP(dom_xml_output_qname(ctx->out, &qualified_name));
}
/* 12.5. Otherwise, if prefix is not null, then: */
else if (prefix != NULL) {
@@ -1033,14 +1038,14 @@ static int dom_xml_serialize_element_node(
qualified_name.name = element->name;
/* 12.5.4. Append the value of qualified name to markup. */
- TRY_OR_CLEANUP(dom_xml_output_qname(out, &qualified_name));
+ TRY_OR_CLEANUP(dom_xml_output_qname(ctx->out, &qualified_name));
/* 12.5.5. Append the following to markup, in the order listed: ... */
- TRY_OR_CLEANUP(xmlOutputBufferWriteLit(out, " xmlns:")); /* 12.5.5.1 - 12.5.5.2 */
- TRY_OR_CLEANUP(xmlOutputBufferWriteString(out, (const char *) prefix));
- TRY_OR_CLEANUP(xmlOutputBufferWriteLit(out, "=\""));
- TRY_OR_CLEANUP(dom_xml_common_text_serialization(out, (const char *) ns, true));
- TRY_OR_CLEANUP(xmlOutputBufferWriteLit(out, "\""));
+ TRY_OR_CLEANUP(xmlOutputBufferWriteLit(ctx->out, " xmlns:")); /* 12.5.5.1 - 12.5.5.2 */
+ TRY_OR_CLEANUP(xmlOutputBufferWriteString(ctx->out, (const char *) prefix));
+ TRY_OR_CLEANUP(xmlOutputBufferWriteLit(ctx->out, "=\""));
+ TRY_OR_CLEANUP(dom_xml_common_text_serialization(ctx->out, (const char *) ns, true));
+ TRY_OR_CLEANUP(xmlOutputBufferWriteLit(ctx->out, "\""));
/* 12.5.6. If local default namespace is not null ... (editorial numbering error: https://github.com/w3c/DOM-Parsing/issues/43) */
if (local_default_namespace != NULL) {
@@ -1064,24 +1069,24 @@ static int dom_xml_serialize_element_node(
inherited_ns = ns;
/* 12.6.4. Append the value of qualified name to markup. */
- TRY_OR_CLEANUP(dom_xml_output_qname(out, &qualified_name));
+ TRY_OR_CLEANUP(dom_xml_output_qname(ctx->out, &qualified_name));
/* 12.6.5. Append the following to markup, in the order listed: ... */
- TRY_OR_CLEANUP(xmlOutputBufferWriteLit(out, " xmlns=\"")); /* 12.6.5.1 - 12.6.5.2 */
- TRY_OR_CLEANUP(dom_xml_common_text_serialization(out, (const char *) ns, true));
- TRY_OR_CLEANUP(xmlOutputBufferWriteLit(out, "\""));
+ TRY_OR_CLEANUP(xmlOutputBufferWriteLit(ctx->out, " xmlns=\"")); /* 12.6.5.1 - 12.6.5.2 */
+ TRY_OR_CLEANUP(dom_xml_common_text_serialization(ctx->out, (const char *) ns, true));
+ TRY_OR_CLEANUP(xmlOutputBufferWriteLit(ctx->out, "\""));
}
/* 12.7. Otherwise, the node has a local default namespace that matches ns ... */
else {
qualified_name.name = element->name;
inherited_ns = ns;
- TRY_OR_CLEANUP(dom_xml_output_qname(out, &qualified_name));
+ TRY_OR_CLEANUP(dom_xml_output_qname(ctx->out, &qualified_name));
}
}
/* 13. Append to markup the result of the XML serialization of node's attributes given map, prefix index,
* local prefixes map, ignore namespace definition attribute flag, and require well-formed flag. */
- TRY_OR_CLEANUP(dom_xml_serialize_attributes(out, element, &map, &local_prefixes_map, prefix_index, ignore_namespace_definition_attribute, require_well_formed));
+ TRY_OR_CLEANUP(dom_xml_serialize_attributes(ctx->out, element, &map, &local_prefixes_map, prefix_index, ignore_namespace_definition_attribute, require_well_formed));
/* 14. If ns is the HTML namespace, and the node's list of children is empty, and the node's localName matches
* any one of the following void elements: ... */
@@ -1109,47 +1114,57 @@ static int dom_xml_serialize_element_node(
|| dom_local_name_compare_ex(element, "source", strlen("source"), name_length)
|| dom_local_name_compare_ex(element, "track", strlen("track"), name_length)
|| dom_local_name_compare_ex(element, "wbr", strlen("wbr"), name_length)) {
- TRY_OR_CLEANUP(xmlOutputBufferWriteLit(out, " /"));
+ TRY_OR_CLEANUP(xmlOutputBufferWriteLit(ctx->out, " /"));
skip_end_tag = true;
}
} else {
/* 15. If ns is not the HTML namespace, and the node's list of children is empty,
* then append "/" (U+002F SOLIDUS) to markup and set the skip end tag flag to true. */
- TRY_OR_CLEANUP(xmlOutputBufferWriteLit(out, "/"));
+ TRY_OR_CLEANUP(xmlOutputBufferWriteLit(ctx->out, "/"));
skip_end_tag = true;
}
}
/* 16. Append ">" (U+003E GREATER-THAN SIGN) to markup. */
- TRY_OR_CLEANUP(xmlOutputBufferWriteLit(out, ">"));
+ TRY_OR_CLEANUP(xmlOutputBufferWriteLit(ctx->out, ">"));
/* 17. If the value of skip end tag is true, then return the value of markup and skip the remaining steps. */
if (!skip_end_tag) {
- /* Step 18 deals with template elements which we don't support. */
-
if (should_format) {
indent++;
} else {
indent = -1;
}
+ /* 18. If ns is the HTML namespace, and the node's localName matches the string "template",
+ * then this is a template element.
+ * Append to markup the result of XML serializing a DocumentFragment node. */
+ xmlNodePtr child = NULL;
+ if (php_dom_ns_is_fast(element, php_dom_ns_is_html_magic_token) && xmlStrEqual(element->name, BAD_CAST "template")) {
+ if (ctx->private_data != NULL) {
+ child = php_dom_retrieve_templated_content(ctx->private_data, element);
+ }
+ } else {
+ child = element->children;
+ }
+
/* 19. Otherwise, append to markup the result of running the XML serialization algorithm on each of node's children. */
- for (xmlNodePtr child = element->children; child != NULL; child = child->next) {
+ for (; child != NULL; child = child->next) {
if (should_format) {
- TRY_OR_CLEANUP(dom_xml_output_indents(out, indent));
+ TRY_OR_CLEANUP(dom_xml_output_indents(ctx->out, indent));
}
- TRY_OR_CLEANUP(dom_xml_serialization_algorithm(ctxt, out, &map, child, inherited_ns, prefix_index, indent, require_well_formed));
+ TRY_OR_CLEANUP(dom_xml_serialization_algorithm(ctx, &map, child, inherited_ns, prefix_index, indent, require_well_formed));
}
if (should_format) {
indent--;
- TRY_OR_CLEANUP(dom_xml_output_indents(out, indent));
+ TRY_OR_CLEANUP(dom_xml_output_indents(ctx->out, indent));
}
/* 20. Append the following to markup, in the order listed: */
- TRY_OR_CLEANUP(xmlOutputBufferWriteLit(out, ""));
- TRY_OR_CLEANUP(dom_xml_output_qname(out, &qualified_name));
- TRY_OR_CLEANUP(xmlOutputBufferWriteLit(out, ">"));
+ TRY_OR_CLEANUP(xmlOutputBufferWriteLit(ctx->out, ""));
+ TRY_OR_CLEANUP(dom_xml_output_qname(ctx->out, &qualified_name));
+ TRY_OR_CLEANUP(xmlOutputBufferWriteLit(ctx->out, ">"));
}
/* 21. Return the value of markup.
@@ -1166,8 +1181,7 @@ static int dom_xml_serialize_element_node(
/* https://w3c.github.io/DOM-Parsing/#xml-serializing-a-documentfragment-node */
static int dom_xml_serializing_a_document_fragment_node(
- xmlSaveCtxtPtr ctxt,
- xmlOutputBufferPtr out,
+ dom_xml_serialize_ctx *ctx,
dom_xml_ns_prefix_map *namespace_prefix_map,
xmlNodePtr node,
const xmlChar *namespace,
@@ -1182,7 +1196,7 @@ static int dom_xml_serializing_a_document_fragment_node(
/* 2. For each child child of node, in tree order, run the XML serialization algorithm on the child ... */
xmlNodePtr child = node->children;
while (child != NULL) {
- TRY(dom_xml_serialization_algorithm(ctxt, out, namespace_prefix_map, child, namespace, prefix_index, indent, require_well_formed));
+ TRY(dom_xml_serialization_algorithm(ctx, namespace_prefix_map, child, namespace, prefix_index, indent, require_well_formed));
child = child->next;
}
@@ -1193,8 +1207,7 @@ static int dom_xml_serializing_a_document_fragment_node(
/* https://w3c.github.io/DOM-Parsing/#dfn-xml-serializing-a-document-node */
static int dom_xml_serializing_a_document_node(
- xmlSaveCtxtPtr ctxt,
- xmlOutputBufferPtr out,
+ dom_xml_serialize_ctx *ctx,
dom_xml_ns_prefix_map *namespace_prefix_map,
xmlNodePtr node,
const xmlChar *namespace,
@@ -1210,16 +1223,16 @@ static int dom_xml_serializing_a_document_node(
node->children = NULL;
/* https://github.com/w3c/DOM-Parsing/issues/50 */
- TRY(xmlOutputBufferFlush(out));
- TRY(xmlSaveDoc(ctxt, (xmlDocPtr) node));
- TRY(xmlSaveFlush(ctxt));
+ TRY(xmlOutputBufferFlush(ctx->out));
+ TRY(xmlSaveDoc(ctx->ctxt, (xmlDocPtr) node));
+ TRY(xmlSaveFlush(ctx->ctxt));
node->children = child;
/* 2. For each child child of node, in tree order, run the XML serialization algorithm on the child passing along the provided arguments,
* and append the result to serialized document. */
while (child != NULL) {
- TRY(dom_xml_serialization_algorithm(ctxt, out, namespace_prefix_map, child, namespace, prefix_index, indent, require_well_formed));
+ TRY(dom_xml_serialization_algorithm(ctx, namespace_prefix_map, child, namespace, prefix_index, indent, require_well_formed));
child = child->next;
}
@@ -1230,8 +1243,7 @@ static int dom_xml_serializing_a_document_node(
/* https://w3c.github.io/DOM-Parsing/#dfn-xml-serialization-algorithm */
static int dom_xml_serialization_algorithm(
- xmlSaveCtxtPtr ctxt,
- xmlOutputBufferPtr out,
+ dom_xml_serialize_ctx *ctx,
dom_xml_ns_prefix_map *namespace_prefix_map,
xmlNodePtr node,
const xmlChar *namespace,
@@ -1243,36 +1255,36 @@ static int dom_xml_serialization_algorithm(
/* If node's interface is: */
switch (node->type) {
case XML_ELEMENT_NODE:
- return dom_xml_serialize_element_node(ctxt, out, namespace, namespace_prefix_map, node, prefix_index, indent, require_well_formed);
+ return dom_xml_serialize_element_node(ctx, namespace, namespace_prefix_map, node, prefix_index, indent, require_well_formed);
case XML_DOCUMENT_FRAG_NODE:
- return dom_xml_serializing_a_document_fragment_node(ctxt, out, namespace_prefix_map, node, namespace, prefix_index, indent, require_well_formed);
+ return dom_xml_serializing_a_document_fragment_node(ctx, namespace_prefix_map, node, namespace, prefix_index, indent, require_well_formed);
case XML_HTML_DOCUMENT_NODE:
case XML_DOCUMENT_NODE:
- return dom_xml_serializing_a_document_node(ctxt, out, namespace_prefix_map, node, namespace, prefix_index, indent, require_well_formed);
+ return dom_xml_serializing_a_document_node(ctx, namespace_prefix_map, node, namespace, prefix_index, indent, require_well_formed);
case XML_TEXT_NODE:
- return dom_xml_serialize_text_node(out, node, require_well_formed);
+ return dom_xml_serialize_text_node(ctx->out, node, require_well_formed);
case XML_COMMENT_NODE:
- return dom_xml_serialize_comment_node(out, node, require_well_formed);
+ return dom_xml_serialize_comment_node(ctx->out, node, require_well_formed);
case XML_PI_NODE:
- return dom_xml_serialize_processing_instruction(out, node, require_well_formed);
+ return dom_xml_serialize_processing_instruction(ctx->out, node, require_well_formed);
case XML_CDATA_SECTION_NODE:
- return dom_xml_serialize_cdata_section_node(out, node);
+ return dom_xml_serialize_cdata_section_node(ctx->out, node);
case XML_ATTRIBUTE_NODE:
- return dom_xml_serialize_attribute_node(out, node);
+ return dom_xml_serialize_attribute_node(ctx->out, node);
default:
- TRY(xmlOutputBufferFlush(out));
- TRY(xmlSaveTree(ctxt, node));
- TRY(xmlSaveFlush(ctxt));
+ TRY(xmlOutputBufferFlush(ctx->out));
+ TRY(xmlSaveTree(ctx->ctxt, node));
+ TRY(xmlSaveFlush(ctx->ctxt));
if (node->type == XML_DTD_NODE) {
- return xmlOutputBufferWriteLit(out, "\n");
+ return xmlOutputBufferWriteLit(ctx->out, "\n");
}
return 0;
}
@@ -1281,7 +1293,7 @@ static int dom_xml_serialization_algorithm(
}
/* https://w3c.github.io/DOM-Parsing/#dfn-xml-serialization */
-int dom_xml_serialize(xmlSaveCtxtPtr ctxt, xmlOutputBufferPtr out, xmlNodePtr node, bool format, bool require_well_formed)
+int dom_xml_serialize(xmlSaveCtxtPtr ctxt, xmlOutputBufferPtr out, xmlNodePtr node, bool format, bool require_well_formed, php_dom_private_data *private_data)
{
/* 1. Let namespace be a context namespace with value null. */
const xmlChar *namespace = NULL;
@@ -1297,8 +1309,12 @@ int dom_xml_serialize(xmlSaveCtxtPtr ctxt, xmlOutputBufferPtr out, xmlNodePtr no
unsigned int prefix_index = 1;
/* 5. Return the result of running the XML serialization algorithm ... */
+ dom_xml_serialize_ctx ctx;
+ ctx.out = out;
+ ctx.ctxt = ctxt;
+ ctx.private_data = private_data;
int indent = format ? 0 : -1;
- int result = dom_xml_serialization_algorithm(ctxt, out, &namespace_prefix_map, node, namespace, &prefix_index, indent, require_well_formed);
+ int result = dom_xml_serialization_algorithm(&ctx, &namespace_prefix_map, node, namespace, &prefix_index, indent, require_well_formed);
dom_xml_ns_prefix_map_dtor(&namespace_prefix_map);
diff --git a/ext/dom/xml_serializer.h b/ext/dom/xml_serializer.h
index 55c589a3b31c0..2d5c3bd84277b 100644
--- a/ext/dom/xml_serializer.h
+++ b/ext/dom/xml_serializer.h
@@ -22,6 +22,9 @@
#include
#include
-int dom_xml_serialize(xmlSaveCtxtPtr ctx, xmlOutputBufferPtr out, xmlNodePtr node, bool format, bool require_well_formed);
+struct php_dom_private_data;
+typedef struct php_dom_private_data php_dom_private_data;
+
+int dom_xml_serialize(xmlSaveCtxtPtr ctx, xmlOutputBufferPtr out, xmlNodePtr node, bool format, bool require_well_formed, php_dom_private_data *private_data);
#endif
diff --git a/ext/dom/xpath.c b/ext/dom/xpath.c
index 5709d4f10f3fb..831a11be62ea0 100644
--- a/ext/dom/xpath.c
+++ b/ext/dom/xpath.c
@@ -23,6 +23,7 @@
#if defined(HAVE_LIBXML) && defined(HAVE_DOM)
#include "php_dom.h"
#include "namespace_compat.h"
+#include "private_data.h"
#define PHP_DOM_XPATH_QUERY 0
#define PHP_DOM_XPATH_EVALUATE 1
diff --git a/ext/libxml/libxml.c b/ext/libxml/libxml.c
index b12113d14c51e..e0b3b4eed430d 100644
--- a/ext/libxml/libxml.c
+++ b/ext/libxml/libxml.c
@@ -275,7 +275,12 @@ static void php_libxml_node_free(xmlNodePtr node)
xmlFreeDtd(dtd);
break;
}
- case XML_ELEMENT_NODE:
+ case XML_ELEMENT_NODE: {
+ if (node->ns && (((uintptr_t) node->ns->_private) & 1) == LIBXML_NS_TAG_HOOK) {
+ /* Special destruction routine hook should be called because it belongs to a "special" namespace. */
+ php_libxml_private_data_header *header = (php_libxml_private_data_header *) (((uintptr_t) node->ns->_private) & ~1);
+ header->ns_hook(header, node);
+ }
if (node->nsDef && node->doc) {
/* Make the namespace declaration survive the destruction of the holding element.
* This prevents a use-after-free on the namespace declaration.
@@ -307,6 +312,7 @@ static void php_libxml_node_free(xmlNodePtr node)
}
xmlFreeNode(node);
break;
+ }
default:
xmlFreeNode(node);
break;
@@ -1368,6 +1374,9 @@ PHP_LIBXML_API int php_libxml_decrement_doc_ref_directly(php_libxml_ref_obj *doc
{
int ret_refcount = --document->refcount;
if (ret_refcount == 0) {
+ if (document->private_data != NULL) {
+ document->private_data->dtor(document->private_data);
+ }
if (document->ptr != NULL) {
xmlFreeDoc((xmlDoc *) document->ptr);
}
@@ -1378,9 +1387,6 @@ PHP_LIBXML_API int php_libxml_decrement_doc_ref_directly(php_libxml_ref_obj *doc
}
efree(document->doc_props);
}
- if (document->private_data != NULL) {
- document->private_data->dtor(document->private_data);
- }
efree(document);
}
diff --git a/ext/libxml/php_libxml.h b/ext/libxml/php_libxml.h
index 95f2cd42d8225..9c2682504e18f 100644
--- a/ext/libxml/php_libxml.h
+++ b/ext/libxml/php_libxml.h
@@ -39,6 +39,8 @@ extern zend_module_entry libxml_module_entry;
#define LIBXML_SAVE_NOEMPTYTAG 1<<2
+#define LIBXML_NS_TAG_HOOK 1
+
ZEND_BEGIN_MODULE_GLOBALS(libxml)
zval stream_context;
smart_str error_buffer;
@@ -67,6 +69,7 @@ typedef struct {
typedef struct php_libxml_private_data_header {
void (*dtor)(struct php_libxml_private_data_header *);
+ void (*ns_hook)(struct php_libxml_private_data_header *, xmlNodePtr);
/* extra fields */
} php_libxml_private_data_header;