Skip to content

Add Dom\Element::insertAdjacentHTML() #16614

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
92 changes: 92 additions & 0 deletions ext/dom/element.c
Original file line number Diff line number Diff line change
Expand Up @@ -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]) {
Copy link
Contributor

@divinity76 divinity76 Nov 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wtf
(edit: yes i see it, avoid strcmp for performance~)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar trick is done in ext/random btw if you're curious

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We really need a mechanism to solve this in a generic fashion. The RoundingMode enum uses the same trick.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. It would be useful to be able to compare enum elements, or at least their name, by address.

Maybe we can store the address of enum elements in some struct once they are instantiated. Storing them in the zend_class_entry would be ideal, but I'm not sure this is doable. Case classes may help with that. Alternatively we could store them in a separate struct generated in _arginfo.h.

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:
*/
Expand Down
2 changes: 1 addition & 1 deletion ext/dom/inner_outer_html_mixin.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions ext/dom/php_dom.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions ext/dom/php_dom.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion ext/dom/php_dom_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
--TEST--
Dom\Element::insertAdjacentHTML() with HTML nodes
--EXTENSIONS--
dom
--FILE--
<?php

const POSITIONS = [
Dom\AdjacentPosition::BeforeBegin,
Dom\AdjacentPosition::AfterBegin,
Dom\AdjacentPosition::BeforeEnd,
Dom\AdjacentPosition::AfterEnd,
];

function test(string $html) {
echo "=== HTML ($html) ===\n";

foreach (POSITIONS as $position) {
echo "--- Position ", $position->name, " ---\n";

$dom = Dom\HTMLDocument::createFromString("<div></div>", 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("<p>foo</p><p>bar</p>");
test("text");
test("");

?>
--EXPECT--
=== HTML (<p>foo</p><p>bar</p>) ===
--- Position BeforeBegin ---
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body><p>foo</p><p>bar</p><div>Sample text</div></body></html>
<html><head></head><body><p>foo</p><p>bar</p><div>Sample text</div></body></html>
int(1)
int(3)
--- Position AfterBegin ---
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body><div><p>foo</p><p>bar</p>Sample text</div></body></html>
<html><head></head><body><div><p>foo</p><p>bar</p>Sample text</div></body></html>
int(3)
int(1)
--- Position BeforeEnd ---
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body><div>Sample text<p>foo</p><p>bar</p></div></body></html>
<html><head></head><body><div>Sample text<p>foo</p><p>bar</p></div></body></html>
int(3)
int(1)
--- Position AfterEnd ---
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body><div>Sample text</div><p>foo</p><p>bar</p></body></html>
<html><head></head><body><div>Sample text</div><p>foo</p><p>bar</p></body></html>
int(1)
int(3)
=== HTML (text) ===
--- Position BeforeBegin ---
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>text<div>Sample text</div></body></html>
<html><head></head><body>text<div>Sample text</div></body></html>
int(1)
int(2)
--- Position AfterBegin ---
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body><div>textSample text</div></body></html>
<html><head></head><body><div>textSample text</div></body></html>
int(2)
int(1)
--- Position BeforeEnd ---
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body><div>Sample texttext</div></body></html>
<html><head></head><body><div>Sample texttext</div></body></html>
int(2)
int(1)
--- Position AfterEnd ---
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body><div>Sample text</div>text</body></html>
<html><head></head><body><div>Sample text</div>text</body></html>
int(1)
int(2)
=== HTML () ===
--- Position BeforeBegin ---
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body><div>Sample text</div></body></html>
<html><head></head><body><div>Sample text</div></body></html>
int(1)
int(1)
--- Position AfterBegin ---
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body><div>Sample text</div></body></html>
<html><head></head><body><div>Sample text</div></body></html>
int(1)
int(1)
--- Position BeforeEnd ---
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body><div>Sample text</div></body></html>
<html><head></head><body><div>Sample text</div></body></html>
int(1)
int(1)
--- Position AfterEnd ---
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body><div>Sample text</div></body></html>
<html><head></head><body><div>Sample text</div></body></html>
int(1)
int(1)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
--TEST--
Dom\Element::insertAdjacentHTML() with HTML nodes - edge case
--EXTENSIONS--
dom
--FILE--
<?php

$dom = Dom\HTMLDocument::createFromString("", LIBXML_NOERROR);

$fragment = $dom->createDocumentFragment();
$node = $fragment->appendChild($dom->createElement("node"));

$node->insertAdjacentHTML(Dom\AdjacentPosition::BeforeBegin, "<p>foo</p>");

echo $dom->saveHtml($fragment), "\n";

$dom->firstChild->insertAdjacentHTML(Dom\AdjacentPosition::AfterBegin, $node->outerHTML);

echo $dom->saveHtml(), "\n";

?>
--EXPECT--
<p>foo</p><node></node>
<html><node></node><head></head><body></body></html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
--TEST--
Dom\Element::insertAdjacentHTML() with HTML nodes - error conditions
--EXTENSIONS--
dom
--FILE--
<?php

$dom = Dom\HTMLDocument::createEmpty();
$element = $dom->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
Loading