From 64083ffea0c4aadc4f48a4f384a371ad4efd4f57 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sat, 26 Oct 2024 14:58:15 +0200 Subject: [PATCH] Add Dom\Element::insertAdjacentHTML() --- NEWS | 1 + UPGRADING | 3 + ext/dom/element.c | 92 ++++++++++++++ ext/dom/inner_outer_html_mixin.c | 2 +- ext/dom/php_dom.h | 1 + ext/dom/php_dom.stub.php | 1 + ext/dom/php_dom_arginfo.h | 9 +- .../Dom_Element_insertAdjacentHTML.phpt | 114 ++++++++++++++++++ ..._Element_insertAdjacentHTML_edge_case.phpt | 24 ++++ ...Dom_Element_insertAdjacentHTML_errors.phpt | 54 +++++++++ .../xml/Element_insertAdjacentHTML.phpt | 101 ++++++++++++++++ .../Element_insertAdjacentHTML_errors.phpt | 17 +++ 12 files changed, 417 insertions(+), 2 deletions(-) create mode 100644 ext/dom/tests/modern/html/interactions/Dom_Element_insertAdjacentHTML.phpt create mode 100644 ext/dom/tests/modern/html/interactions/Dom_Element_insertAdjacentHTML_edge_case.phpt create mode 100644 ext/dom/tests/modern/html/interactions/Dom_Element_insertAdjacentHTML_errors.phpt create mode 100644 ext/dom/tests/modern/xml/Element_insertAdjacentHTML.phpt create mode 100644 ext/dom/tests/modern/xml/Element_insertAdjacentHTML_errors.phpt diff --git a/NEWS b/NEWS index c59c9a967c40e..37f394d117976 100644 --- a/NEWS +++ b/NEWS @@ -10,6 +10,7 @@ PHP NEWS - DOM: . Added Dom\Element::$outerHTML. (nielsdos) + . Added Dom\Element::insertAdjacentHTML(). (nielsdos) - Output: . Fixed calculation of aligned buffer size. (cmb) diff --git a/UPGRADING b/UPGRADING index a336173ffd8b0..c4cf90eb6ef1a 100644 --- a/UPGRADING +++ b/UPGRADING @@ -88,6 +88,9 @@ PHP 8.5 UPGRADE NOTES attached to a CurlMultiHandle. This includes both handles added using curl_multi_add_handle() and handles accepted by CURLMOPT_PUSHFUNCTION. +- DOM: + . Added Dom\Element::insertAdjacentHTML(). + - PGSQL: . pg_close_stmt offers an alternative way to close a prepared statement from the DEALLOCATE sql command in that we can reuse diff --git a/ext/dom/element.c b/ext/dom/element.c index 418096312c456..e96f4547d8f3f 100644 --- a/ext/dom/element.c +++ b/ext/dom/element.c @@ -1715,6 +1715,98 @@ PHP_METHOD(Dom_Element, insertAdjacentText) } /* }}} end DOMElement::insertAdjacentText */ +/* https://html.spec.whatwg.org/#dom-element-insertadjacenthtml */ +PHP_METHOD(Dom_Element, insertAdjacentHTML) +{ + zval *where_zv; + zend_string *string; + + dom_object *this_intern; + zval *id; + xmlNodePtr thisp; + + bool created_context = false; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT_OF_CLASS(where_zv, dom_adjacent_position_class_entry) + Z_PARAM_STR(string) + ZEND_PARSE_PARAMETERS_END(); + + DOM_GET_THIS_OBJ(thisp, id, xmlNodePtr, this_intern); + + const zend_string *where = Z_STR_P(zend_enum_fetch_case_name(Z_OBJ_P(where_zv))); + + /* 1. We don't do injection sinks. */ + + /* 2. Let context be NULL */ + xmlNodePtr context = NULL; + + /* 3. Use the first matching item from this list: (...) */ + switch (ZSTR_LEN(where) + ZSTR_VAL(where)[2]) { + case sizeof("BeforeBegin") - 1 + 'f': + case sizeof("AfterEnd") - 1 + 't': + /* 1. Set context to this's parent. */ + context = thisp->parent; + + /* 2. If context is null or a Document, throw a "NoModificationAllowedError" DOMException. */ + if (context == NULL || context->type == XML_DOCUMENT_NODE || context->type == XML_HTML_DOCUMENT_NODE) { + php_dom_throw_error(NO_MODIFICATION_ALLOWED_ERR, true); + RETURN_THROWS(); + } + break; + case sizeof("AfterBegin") - 1 + 't': + case sizeof("BeforeEnd") - 1 + 'f': + /* Set context to this. */ + context = thisp; + break; + EMPTY_SWITCH_DEFAULT_CASE(); + } + + /* 4. If context is not an Element or all of the following are true: (...) */ + if (context->type != XML_ELEMENT_NODE + || (php_dom_ns_is_html_and_document_is_html(context) && xmlStrEqual(context->name, BAD_CAST "html"))) { + /* set context to the result of creating an element given this's node document, body, and the HTML namespace. */ + xmlNsPtr html_ns = php_dom_libxml_ns_mapper_ensure_html_ns(php_dom_get_ns_mapper(this_intern)); + + context = xmlNewDocNode(thisp->doc, html_ns, BAD_CAST "body", NULL); + created_context = true; + if (UNEXPECTED(context == NULL)) { + php_dom_throw_error(INVALID_STATE_ERR, true); + goto err; + } + } + + /* 5. Let fragment be the result of invoking the fragment parsing algorithm steps with context and compliantString. */ + xmlNodePtr fragment = dom_parse_fragment(this_intern, context, string); + if (fragment == NULL) { + goto err; + } + + php_libxml_invalidate_node_list_cache(this_intern->document); + + /* 6. Use the first matching item from this list: (...) */ + switch (ZSTR_LEN(where) + ZSTR_VAL(where)[2]) { + case sizeof("BeforeBegin") - 1 + 'f': + php_dom_pre_insert(this_intern->document, fragment, thisp->parent, thisp); + break; + case sizeof("AfterEnd") - 1 + 't': + php_dom_pre_insert(this_intern->document, fragment, thisp->parent, thisp->next); + break; + case sizeof("AfterBegin") - 1 + 't': + php_dom_pre_insert(this_intern->document, fragment, thisp, thisp->children); + break; + case sizeof("BeforeEnd") - 1 + 'f': + php_dom_node_append(this_intern->document, fragment, thisp); + break; + EMPTY_SWITCH_DEFAULT_CASE(); + } + +err: + if (created_context) { + xmlFreeNode(context); + } +} + /* {{{ URL: https://dom.spec.whatwg.org/#dom-element-toggleattribute Since: */ diff --git a/ext/dom/inner_outer_html_mixin.c b/ext/dom/inner_outer_html_mixin.c index 37283ef351116..b14c3ba708ffb 100644 --- a/ext/dom/inner_outer_html_mixin.c +++ b/ext/dom/inner_outer_html_mixin.c @@ -342,7 +342,7 @@ static xmlNodePtr dom_xml_fragment_parsing_algorithm(dom_object *obj, const xmlN } /* https://w3c.github.io/DOM-Parsing/#dfn-fragment-parsing-algorithm */ -static xmlNodePtr dom_parse_fragment(dom_object *obj, xmlNodePtr context_node, const zend_string *input) +xmlNodePtr dom_parse_fragment(dom_object *obj, xmlNodePtr context_node, const zend_string *input) { if (context_node->doc->type == XML_DOCUMENT_NODE) { return dom_xml_fragment_parsing_algorithm(obj, context_node, input); diff --git a/ext/dom/php_dom.h b/ext/dom/php_dom.h index 851bc14d12574..e4013e448ae26 100644 --- a/ext/dom/php_dom.h +++ b/ext/dom/php_dom.h @@ -211,6 +211,7 @@ void dom_parent_node_query_selector(xmlNodePtr thisp, dom_object *intern, zval * void dom_parent_node_query_selector_all(xmlNodePtr thisp, dom_object *intern, zval *return_value, const zend_string *selectors_str); void dom_element_matches(xmlNodePtr thisp, dom_object *intern, zval *return_value, const zend_string *selectors_str); void dom_element_closest(xmlNodePtr thisp, dom_object *intern, zval *return_value, const zend_string *selectors_str); +xmlNodePtr dom_parse_fragment(dom_object *obj, xmlNodePtr context_node, const zend_string *input); /* nodemap and nodelist APIs */ xmlNodePtr php_dom_named_node_map_get_named_item(dom_nnodemap_object *objmap, const zend_string *named, bool may_transform); diff --git a/ext/dom/php_dom.stub.php b/ext/dom/php_dom.stub.php index 587694f5dfa70..ac11623471f19 100644 --- a/ext/dom/php_dom.stub.php +++ b/ext/dom/php_dom.stub.php @@ -1632,6 +1632,7 @@ public function getElementsByTagNameNS(?string $namespace, string $localName): H public function insertAdjacentElement(AdjacentPosition $where, Element $element): ?Element {} public function insertAdjacentText(AdjacentPosition $where, string $data): void {} + public function insertAdjacentHTML(AdjacentPosition $where, string $string): void {} /** * @readonly diff --git a/ext/dom/php_dom_arginfo.h b/ext/dom/php_dom_arginfo.h index 3d6f093eb22e9..08dbf22a9b91c 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: 20c13a727cffb452475989a743ec29a8412a52f1 */ + * Stub hash: 860bf40a97ec6570e9f5b0616407ba55d7c8d6f8 */ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_dom_import_simplexml, 0, 1, DOMAttr|DOMElement, 0) ZEND_ARG_TYPE_INFO(0, node, IS_OBJECT, 0) @@ -785,6 +785,11 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Dom_Element_insertAdjacent ZEND_ARG_TYPE_INFO(0, data, IS_STRING, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Dom_Element_insertAdjacentHTML, 0, 2, IS_VOID, 0) + ZEND_ARG_OBJ_INFO(0, where, Dom\\AdjacentPosition, 0) + ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Dom_Element_setIdAttribute, 0, 2, IS_VOID, 0) ZEND_ARG_TYPE_INFO(0, qualifiedName, IS_STRING, 0) ZEND_ARG_TYPE_INFO(0, isId, _IS_BOOL, 0) @@ -1275,6 +1280,7 @@ ZEND_METHOD(Dom_Element, getElementsByTagName); ZEND_METHOD(Dom_Element, getElementsByTagNameNS); ZEND_METHOD(Dom_Element, insertAdjacentElement); ZEND_METHOD(Dom_Element, insertAdjacentText); +ZEND_METHOD(Dom_Element, insertAdjacentHTML); ZEND_METHOD(Dom_Element, setIdAttributeNode); ZEND_METHOD(Dom_Element, querySelector); ZEND_METHOD(Dom_Element, querySelectorAll); @@ -1649,6 +1655,7 @@ static const zend_function_entry class_Dom_Element_methods[] = { ZEND_ME(Dom_Element, getElementsByTagNameNS, arginfo_class_Dom_Element_getElementsByTagNameNS, ZEND_ACC_PUBLIC) ZEND_ME(Dom_Element, insertAdjacentElement, arginfo_class_Dom_Element_insertAdjacentElement, ZEND_ACC_PUBLIC) ZEND_ME(Dom_Element, insertAdjacentText, arginfo_class_Dom_Element_insertAdjacentText, ZEND_ACC_PUBLIC) + ZEND_ME(Dom_Element, insertAdjacentHTML, arginfo_class_Dom_Element_insertAdjacentHTML, ZEND_ACC_PUBLIC) ZEND_RAW_FENTRY("setIdAttribute", zim_DOMElement_setIdAttribute, arginfo_class_Dom_Element_setIdAttribute, ZEND_ACC_PUBLIC, NULL, NULL) ZEND_RAW_FENTRY("setIdAttributeNS", zim_DOMElement_setIdAttributeNS, arginfo_class_Dom_Element_setIdAttributeNS, ZEND_ACC_PUBLIC, NULL, NULL) ZEND_ME(Dom_Element, setIdAttributeNode, arginfo_class_Dom_Element_setIdAttributeNode, ZEND_ACC_PUBLIC) diff --git a/ext/dom/tests/modern/html/interactions/Dom_Element_insertAdjacentHTML.phpt b/ext/dom/tests/modern/html/interactions/Dom_Element_insertAdjacentHTML.phpt new file mode 100644 index 0000000000000..a2c9471e669f0 --- /dev/null +++ b/ext/dom/tests/modern/html/interactions/Dom_Element_insertAdjacentHTML.phpt @@ -0,0 +1,114 @@ +--TEST-- +Dom\Element::insertAdjacentHTML() with HTML nodes +--EXTENSIONS-- +dom +--FILE-- +name, " ---\n"; + + $dom = Dom\HTMLDocument::createFromString("
", LIBXML_NOERROR); + $div = $dom->body->firstChild; + $div->append("Sample text"); + + $div->insertAdjacentHTML($position, $html); + + echo $dom->saveXML(), "\n"; + echo $dom->saveHTML(), "\n"; + var_dump($div->childNodes->length); + var_dump($dom->body->childNodes->length); + } +} + +test("

foo

bar

"); +test("text"); +test(""); + +?> +--EXPECT-- +=== HTML (

foo

bar

) === +--- Position BeforeBegin --- + +

foo

bar

Sample text
+

foo

bar

Sample text
+int(1) +int(3) +--- Position AfterBegin --- + +

foo

bar

Sample text
+

foo

bar

Sample text
+int(3) +int(1) +--- Position BeforeEnd --- + +
Sample text

foo

bar

+
Sample text

foo

bar

+int(3) +int(1) +--- Position AfterEnd --- + +
Sample text

foo

bar

+
Sample text

foo

bar

+int(1) +int(3) +=== HTML (text) === +--- Position BeforeBegin --- + +text
Sample text
+text
Sample text
+int(1) +int(2) +--- Position AfterBegin --- + +
textSample text
+
textSample text
+int(2) +int(1) +--- Position BeforeEnd --- + +
Sample texttext
+
Sample texttext
+int(2) +int(1) +--- Position AfterEnd --- + +
Sample text
text +
Sample text
text +int(1) +int(2) +=== HTML () === +--- Position BeforeBegin --- + +
Sample text
+
Sample text
+int(1) +int(1) +--- Position AfterBegin --- + +
Sample text
+
Sample text
+int(1) +int(1) +--- Position BeforeEnd --- + +
Sample text
+
Sample text
+int(1) +int(1) +--- Position AfterEnd --- + +
Sample text
+
Sample text
+int(1) +int(1) diff --git a/ext/dom/tests/modern/html/interactions/Dom_Element_insertAdjacentHTML_edge_case.phpt b/ext/dom/tests/modern/html/interactions/Dom_Element_insertAdjacentHTML_edge_case.phpt new file mode 100644 index 0000000000000..f3f47eebf3319 --- /dev/null +++ b/ext/dom/tests/modern/html/interactions/Dom_Element_insertAdjacentHTML_edge_case.phpt @@ -0,0 +1,24 @@ +--TEST-- +Dom\Element::insertAdjacentHTML() with HTML nodes - edge case +--EXTENSIONS-- +dom +--FILE-- +createDocumentFragment(); +$node = $fragment->appendChild($dom->createElement("node")); + +$node->insertAdjacentHTML(Dom\AdjacentPosition::BeforeBegin, "

foo

"); + +echo $dom->saveHtml($fragment), "\n"; + +$dom->firstChild->insertAdjacentHTML(Dom\AdjacentPosition::AfterBegin, $node->outerHTML); + +echo $dom->saveHtml(), "\n"; + +?> +--EXPECT-- +

foo

+ diff --git a/ext/dom/tests/modern/html/interactions/Dom_Element_insertAdjacentHTML_errors.phpt b/ext/dom/tests/modern/html/interactions/Dom_Element_insertAdjacentHTML_errors.phpt new file mode 100644 index 0000000000000..b8e13ba436b6b --- /dev/null +++ b/ext/dom/tests/modern/html/interactions/Dom_Element_insertAdjacentHTML_errors.phpt @@ -0,0 +1,54 @@ +--TEST-- +Dom\Element::insertAdjacentHTML() with HTML nodes - error conditions +--EXTENSIONS-- +dom +--FILE-- +createElement('root'); + +echo "--- BeforeBegin no parent ---\n"; + +try { + $element->insertAdjacentHTML(Dom\AdjacentPosition::BeforeBegin, "test"); +} catch (DOMException $e) { + echo $e->getMessage(), "\n"; +} + +echo "--- AfterEnd no parent ---\n"; + +try { + $element->insertAdjacentHTML(Dom\AdjacentPosition::AfterEnd, "test"); +} catch (DOMException $e) { + echo $e->getMessage(), "\n"; +} + +$dom->appendChild($element); + +echo "--- BeforeBegin document parent ---\n"; + +try { + $element->insertAdjacentHTML(Dom\AdjacentPosition::BeforeBegin, "test"); +} catch (DOMException $e) { + echo $e->getMessage(), "\n"; +} + +echo "--- AfterEnd document parent ---\n"; + +try { + $element->insertAdjacentHTML(Dom\AdjacentPosition::AfterEnd, "test"); +} catch (DOMException $e) { + echo $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +--- BeforeBegin no parent --- +No Modification Allowed Error +--- AfterEnd no parent --- +No Modification Allowed Error +--- BeforeBegin document parent --- +No Modification Allowed Error +--- AfterEnd document parent --- +No Modification Allowed Error diff --git a/ext/dom/tests/modern/xml/Element_insertAdjacentHTML.phpt b/ext/dom/tests/modern/xml/Element_insertAdjacentHTML.phpt new file mode 100644 index 0000000000000..a233e01759f6f --- /dev/null +++ b/ext/dom/tests/modern/xml/Element_insertAdjacentHTML.phpt @@ -0,0 +1,101 @@ +--TEST-- +Dom\Element::insertAdjacentHTML() with XML nodes +--EXTENSIONS-- +dom +--FILE-- +name, " ---\n"; + + $dom = Dom\XMLDocument::createFromString('
'); + $div = $dom->documentElement->firstChild; + $div->append("Sample text"); + + $div->insertAdjacentHTML($position, $xml); + + echo $dom->saveXML(), "\n"; + var_dump($div->childNodes->length); + var_dump($dom->documentElement->childNodes->length); + } +} + +test(''); +test('&'); +test('text node'); + +?> +--EXPECT-- +=== XML () === +--- Position BeforeBegin --- + +
Sample text
+int(1) +int(3) +--- Position AfterBegin --- + +
Sample text
+int(3) +int(1) +--- Position BeforeEnd --- + +
Sample text
+int(3) +int(1) +--- Position AfterEnd --- + +
Sample text
+int(1) +int(3) +=== XML (&) === +--- Position BeforeBegin --- + +&
Sample text
+int(1) +int(4) +--- Position AfterBegin --- + +
&Sample text
+int(4) +int(1) +--- Position BeforeEnd --- + +
Sample text&
+int(4) +int(1) +--- Position AfterEnd --- + +
Sample text
&
+int(1) +int(4) +=== XML (text node) === +--- Position BeforeBegin --- + +text node
Sample text
+int(1) +int(2) +--- Position AfterBegin --- + +
text nodeSample text
+int(2) +int(1) +--- Position BeforeEnd --- + +
Sample texttext node
+int(2) +int(1) +--- Position AfterEnd --- + +
Sample text
text node
+int(1) +int(2) diff --git a/ext/dom/tests/modern/xml/Element_insertAdjacentHTML_errors.phpt b/ext/dom/tests/modern/xml/Element_insertAdjacentHTML_errors.phpt new file mode 100644 index 0000000000000..0ecd1d4873605 --- /dev/null +++ b/ext/dom/tests/modern/xml/Element_insertAdjacentHTML_errors.phpt @@ -0,0 +1,17 @@ +--TEST-- +Dom\Element::insertAdjacentHTML() with XML nodes - errors +--EXTENSIONS-- +dom +--FILE-- +'); +try { + $dom->documentElement->insertAdjacentHTML(Dom\AdjacentPosition::AfterBegin, ""); +} catch (DOMException $e) { + echo $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +XML fragment is not well-formed