diff --git a/ext/dom/config.m4 b/ext/dom/config.m4 index b279bccd5bda..c6c67ced36e5 100644 --- a/ext/dom/config.m4 +++ b/ext/dom/config.m4 @@ -206,7 +206,7 @@ if test "$PHP_DOM" != "no"; then html5_parser.c html5_serializer.c infra.c - inner_html_mixin.c + inner_outer_html_mixin.c namednodemap.c namespace_compat.c node.c diff --git a/ext/dom/config.w32 b/ext/dom/config.w32 index 231f005895f5..1db4f6d11ba1 100644 --- a/ext/dom/config.w32 +++ b/ext/dom/config.w32 @@ -10,7 +10,7 @@ if (PHP_DOM == "yes") { 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 private_data.c \ domexception.c processinginstruction.c \ - cdatasection.c documentfragment.c domimplementation.c element.c inner_html_mixin.c \ + cdatasection.c documentfragment.c domimplementation.c element.c inner_outer_html_mixin.c \ node.c characterdata.c documenttype.c \ entity.c nodelist.c html_collection.c text.c comment.c \ entityreference.c \ diff --git a/ext/dom/dom_properties.h b/ext/dom/dom_properties.h index 338b4acc449f..d711aa1a2e55 100644 --- a/ext/dom/dom_properties.h +++ b/ext/dom/dom_properties.h @@ -86,6 +86,8 @@ zend_result dom_element_id_write(dom_object *obj, zval *newval); zend_result dom_element_schema_type_info_read(dom_object *obj, zval *retval); zend_result dom_element_inner_html_read(dom_object *obj, zval *retval); zend_result dom_element_inner_html_write(dom_object *obj, zval *newval); +zend_result dom_element_outer_html_read(dom_object *obj, zval *retval); +zend_result dom_element_outer_html_write(dom_object *obj, zval *newval); zend_result dom_element_class_list_read(dom_object *obj, zval *retval); zend_result dom_modern_element_substituted_node_value_read(dom_object *obj, zval *retval); zend_result dom_modern_element_substituted_node_value_write(dom_object *obj, zval *newval); diff --git a/ext/dom/inner_html_mixin.c b/ext/dom/inner_outer_html_mixin.c similarity index 77% rename from ext/dom/inner_html_mixin.c rename to ext/dom/inner_outer_html_mixin.c index 262c85411aaf..4655878c533f 100644 --- a/ext/dom/inner_html_mixin.c +++ b/ext/dom/inner_outer_html_mixin.c @@ -55,12 +55,9 @@ static int dom_write_smart_str(void *context, const char *buffer, int len) return len; } -/* https://w3c.github.io/DOM-Parsing/#the-innerhtml-mixin - * and https://w3c.github.io/DOM-Parsing/#dfn-fragment-serializing-algorithm */ -zend_result dom_element_inner_html_read(dom_object *obj, zval *retval) +/* https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#fragment-serializing-algorithm-steps */ +static zend_string *dom_element_html_fragment_serialize(dom_object *obj, xmlNodePtr node) { - DOM_PROP_NODE(xmlNodePtr, node, obj); - /* 1. Let context document be the value of node's node document. */ const xmlDoc *context_document = node->doc; @@ -73,7 +70,7 @@ zend_result dom_element_inner_html_read(dom_object *obj, zval *retval) ctx.write_string = dom_inner_html_write_string; ctx.write_string_len = dom_inner_html_write_string_len; dom_html5_serialize(&ctx, node); - ZVAL_STR(retval, smart_str_extract(&output)); + return smart_str_extract(&output); } /* 3. Otherwise, context document is an XML document; return an XML serialization of node passing the flag require well-formed. */ else { @@ -104,11 +101,21 @@ zend_result dom_element_inner_html_read(dom_object *obj, zval *retval) if (UNEXPECTED(status < 0)) { smart_str_free_ex(&str, false); php_dom_throw_error_with_message(SYNTAX_ERR, "The resulting XML serialization is not well-formed", true); - return FAILURE; + return NULL; } - ZVAL_STR(retval, smart_str_extract(&str)); + return smart_str_extract(&str); } +} +/* https://w3c.github.io/DOM-Parsing/#the-innerhtml-mixin */ +zend_result dom_element_inner_html_read(dom_object *obj, zval *retval) +{ + DOM_PROP_NODE(xmlNodePtr, node, obj); + zend_string *serialization = dom_element_html_fragment_serialize(obj, node); + if (serialization == NULL) { + return FAILURE; + } + ZVAL_STR(retval, serialization); return SUCCESS; } @@ -334,23 +341,31 @@ static xmlNodePtr dom_xml_fragment_parsing_algorithm(dom_object *obj, const xmlN return NULL; } -/* https://w3c.github.io/DOM-Parsing/#the-innerhtml-mixin - * and https://w3c.github.io/DOM-Parsing/#dfn-fragment-parsing-algorithm */ -zend_result dom_element_inner_html_write(dom_object *obj, zval *newval) +/* 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) { - DOM_PROP_NODE(xmlNodePtr, context_node, obj); - - xmlNodePtr fragment; if (context_node->doc->type == XML_DOCUMENT_NODE) { - fragment = dom_xml_fragment_parsing_algorithm(obj, context_node, Z_STR_P(newval)); + return dom_xml_fragment_parsing_algorithm(obj, context_node, input); } else { - fragment = dom_html_fragment_parsing_algorithm(obj, context_node, Z_STR_P(newval), obj->document->quirks_mode); + return dom_html_fragment_parsing_algorithm(obj, context_node, input, obj->document->quirks_mode); } +} +/* https://w3c.github.io/DOM-Parsing/#the-innerhtml-mixin */ +zend_result dom_element_inner_html_write(dom_object *obj, zval *newval) +{ + /* 1. We don't do injection sinks, skip. */ + + /* 2. Let context be this. */ + DOM_PROP_NODE(xmlNodePtr, context_node, obj); + + /* 3. Let fragment be the result of invoking the fragment parsing algorithm steps with context and compliantString. */ + xmlNodePtr fragment = dom_parse_fragment(obj, context_node, Z_STR_P(newval)); if (fragment == NULL) { return FAILURE; } + /* 4. If context is a template element, then set context to the template element's template contents (a DocumentFragment). */ 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) { @@ -359,8 +374,94 @@ zend_result dom_element_inner_html_write(dom_object *obj, zval *newval) } } + /* 5. Replace all with fragment within context. */ dom_remove_all_children(context_node); return php_dom_pre_insert(obj->document, fragment, context_node, NULL) ? SUCCESS : FAILURE; } +/* https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#the-outerhtml-property */ +zend_result dom_element_outer_html_read(dom_object *obj, zval *retval) +{ + DOM_PROP_NODE(xmlNodePtr, this, obj); + + /* 1. Let element be a fictional node whose only child is this. */ + xmlNode element; + memset(&element, 0, sizeof(element)); + element.type = XML_DOCUMENT_FRAG_NODE; + element.children = element.last = this; + element.doc = this->doc; + + xmlNodePtr old_parent = this->parent; + this->parent = &element; + + /* 2. Return the result of running fragment serializing algorithm steps with element and true. */ + zend_string *serialization = dom_element_html_fragment_serialize(obj, &element); + + this->parent = old_parent; + + if (serialization == NULL) { + return FAILURE; + } + ZVAL_STR(retval, serialization); + return SUCCESS; +} + +/* https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#the-outerhtml-property */ +zend_result dom_element_outer_html_write(dom_object *obj, zval *newval) +{ + /* 1. We don't do injection sinks, skip. */ + + /* 2. Let parent be this's parent. */ + DOM_PROP_NODE(xmlNodePtr, this, obj); + xmlNodePtr parent = this->parent; + bool created_parent = false; + + /* 3. If parent is null, return. */ + if (parent == NULL) { + return SUCCESS; + } + + /* 4. If parent is a Document, throw. */ + if (parent->type == XML_DOCUMENT_NODE || parent->type == XML_HTML_DOCUMENT_NODE) { + php_dom_throw_error(INVALID_MODIFICATION_ERR, true); + return FAILURE; + } + + /* 5. If parent is a DocumentFragment, set parent to the result of creating an element given this's node document, body, and the HTML namespace. */ + if (parent->type == XML_DOCUMENT_FRAG_NODE) { + xmlNsPtr html_ns = php_dom_libxml_ns_mapper_ensure_html_ns(php_dom_get_ns_mapper(obj)); + + parent = xmlNewDocNode(parent->doc, html_ns, BAD_CAST "body", NULL); + created_parent = true; + if (UNEXPECTED(parent == NULL)) { + php_dom_throw_error(INVALID_STATE_ERR, true); + return FAILURE; + } + } + + /* 6. Let fragment be the result of invoking the fragment parsing algorithm steps given parent and compliantString. */ + xmlNodePtr fragment = dom_parse_fragment(obj, parent, Z_STR_P(newval)); + if (fragment == NULL) { + if (created_parent) { + xmlFreeNode(parent); + } + return FAILURE; + } + + /* 7. Replace this with fragment within this's parent. */ + if (!php_dom_pre_insert(obj->document, fragment, this->parent, this)) { + xmlFreeNode(fragment); + if (created_parent) { + xmlFreeNode(parent); + } + return FAILURE; + } + xmlUnlinkNode(this); + if (created_parent) { + ZEND_ASSERT(parent->children == NULL); + xmlFreeNode(parent); + } + return SUCCESS; +} + #endif diff --git a/ext/dom/php_dom.c b/ext/dom/php_dom.c index 8198afe5b8ba..4745fbcdc294 100644 --- a/ext/dom/php_dom.c +++ b/ext/dom/php_dom.c @@ -1111,6 +1111,7 @@ PHP_MINIT_FUNCTION(dom) DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "previousElementSibling", dom_node_previous_element_sibling_read, NULL); DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "nextElementSibling", dom_node_next_element_sibling_read, NULL); DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "innerHTML", dom_element_inner_html_read, dom_element_inner_html_write); + DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "outerHTML", dom_element_outer_html_read, dom_element_outer_html_write); DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "substitutedNodeValue", dom_modern_element_substituted_node_value_read, dom_modern_element_substituted_node_value_write); zend_hash_merge(&dom_modern_element_prop_handlers, &dom_modern_node_prop_handlers, NULL, false); DOM_OVERWRITE_PROP_HANDLER(&dom_modern_element_prop_handlers, "textContent", dom_node_text_content_read, dom_node_text_content_write); diff --git a/ext/dom/php_dom.stub.php b/ext/dom/php_dom.stub.php index 0f7f74841042..acd507920784 100644 --- a/ext/dom/php_dom.stub.php +++ b/ext/dom/php_dom.stub.php @@ -1688,6 +1688,9 @@ public function matches(string $selectors): bool {} /** @virtual */ public string $innerHTML; + /** @virtual */ + public string $outerHTML; + /** @virtual */ public string $substitutedNodeValue; diff --git a/ext/dom/php_dom_arginfo.h b/ext/dom/php_dom_arginfo.h index e1f230ccbcf0..0fbcba0d74e2 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: b79ad2b70757f7d65a6b4fd907222a4955264bf6 */ + * Stub hash: bc53676bcd060f8fd26ff6e92da4983e85c0eb83 */ 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) @@ -3078,6 +3078,12 @@ static zend_class_entry *register_class_Dom_Element(zend_class_entry *class_entr zend_declare_typed_property(class_entry, property_innerHTML_name, &property_innerHTML_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_VIRTUAL, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); zend_string_release(property_innerHTML_name); + zval property_outerHTML_default_value; + ZVAL_UNDEF(&property_outerHTML_default_value); + zend_string *property_outerHTML_name = zend_string_init("outerHTML", sizeof("outerHTML") - 1, 1); + zend_declare_typed_property(class_entry, property_outerHTML_name, &property_outerHTML_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_VIRTUAL, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + zend_string_release(property_outerHTML_name); + zval property_substitutedNodeValue_default_value; ZVAL_UNDEF(&property_substitutedNodeValue_default_value); zend_string *property_substitutedNodeValue_name = zend_string_init("substitutedNodeValue", sizeof("substitutedNodeValue") - 1, 1); diff --git a/ext/dom/tests/gh15192.phpt b/ext/dom/tests/gh15192.phpt index 5ab5d858eceb..c7bf0a543bb9 100644 --- a/ext/dom/tests/gh15192.phpt +++ b/ext/dom/tests/gh15192.phpt @@ -10,8 +10,8 @@ $element = $dom2->firstChild; $dom = new DomDocument(); var_dump($element); ?> ---EXPECTF-- -object(Dom\HTMLElement)#3 (29) { +--EXPECT-- +object(Dom\HTMLElement)#3 (30) { ["namespaceURI"]=> string(28) "http://www.w3.org/1999/xhtml" ["prefix"]=> @@ -40,6 +40,8 @@ object(Dom\HTMLElement)#3 (29) { NULL ["innerHTML"]=> string(36) "

foo

" + ["outerHTML"]=> + string(49) "

foo

" ["substitutedNodeValue"]=> string(3) "foo" ["nodeType"]=> diff --git a/ext/dom/tests/modern/html/parser/Element_outerHTML.phpt b/ext/dom/tests/modern/html/parser/Element_outerHTML.phpt new file mode 100644 index 000000000000..401e11b16456 --- /dev/null +++ b/ext/dom/tests/modern/html/parser/Element_outerHTML.phpt @@ -0,0 +1,34 @@ +--TEST-- +Test writing Element::$outerHTML on HTML documents +--EXTENSIONS-- +dom +--FILE-- +foo

', LIBXML_NOERROR); +$p = $dom->body->firstChild; +$p->outerHTML = '
 

'; // intentionally unclosed +echo $dom->saveXML(), "\n"; +echo $dom->saveHtml(), "\n"; +$div = $dom->body->firstChild; +$div->outerHTML = "invalid\xffutf-8𐍈𐍈𐍈"; +echo $dom->saveXML(), "\n"; +echo $dom->saveHtml(), "\n"; + +$dom->body->outerHTML = ''; +var_dump($dom->body->querySelector('p')); // Should be NULL because the template contents do not participate in the DOM tree +echo $dom->saveXML(), "\n"; +echo $dom->saveHtml(), "\n"; + +?> +--EXPECT-- + +

 

+
 

+ +invalid�utf-8𐍈𐍈𐍈 

+invalid�utf-8𐍈𐍈𐍈 

+NULL + + + diff --git a/ext/dom/tests/modern/html/serializer/Element_outerHTML.phpt b/ext/dom/tests/modern/html/serializer/Element_outerHTML.phpt new file mode 100644 index 000000000000..a8e780d421ea --- /dev/null +++ b/ext/dom/tests/modern/html/serializer/Element_outerHTML.phpt @@ -0,0 +1,28 @@ +--TEST-- +Test reading Element::$outerHTML on HTML documents +--EXTENSIONS-- +dom +--FILE-- +foo

', LIBXML_NOERROR); + +$p = $dom->body->firstChild; +var_dump($p->outerHTML); + +$root = $dom->documentElement; +var_dump($root->outerHTML); + +$unattached_element = $dom->createElement('unattached'); +var_dump($unattached_element->outerHTML); + +$template = $dom->createElement('template'); +$template->innerHTML = '

foo

'; +var_dump($template->outerHTML); + +?> +--EXPECT-- +string(10) "

foo

" +string(49) "

foo

" +string(25) "" +string(31) "" diff --git a/ext/dom/tests/modern/html/serializer/Element_outerHTML_invalid_tree.phpt b/ext/dom/tests/modern/html/serializer/Element_outerHTML_invalid_tree.phpt new file mode 100644 index 000000000000..4803e2067639 --- /dev/null +++ b/ext/dom/tests/modern/html/serializer/Element_outerHTML_invalid_tree.phpt @@ -0,0 +1,17 @@ +--TEST-- +Test reading Element::$outerHTML on HTML documents - invalid tree variation +--EXTENSIONS-- +dom +--CREDITS-- +Dennis Snell +--FILE-- +

Link

', LIBXML_NOERROR); +$p = $dom->body->querySelector('p'); +$p->outerHTML = 'Another Link'; +echo $dom->saveHTML(); + +?> +--EXPECT-- +Another Link diff --git a/ext/dom/tests/modern/xml/Element_innerHTML_reading.phpt b/ext/dom/tests/modern/xml/Element_innerOuterHTML_reading.phpt similarity index 67% rename from ext/dom/tests/modern/xml/Element_innerHTML_reading.phpt rename to ext/dom/tests/modern/xml/Element_innerOuterHTML_reading.phpt index b096fc2c6cc8..76f2d6fecd20 100644 --- a/ext/dom/tests/modern/xml/Element_innerHTML_reading.phpt +++ b/ext/dom/tests/modern/xml/Element_innerOuterHTML_reading.phpt @@ -1,5 +1,5 @@ --TEST-- -Test reading Element::$innerHTML on XML documents +Test reading Element::${inner,outer}HTML on XML documents --EXTENSIONS-- dom --FILE-- @@ -16,43 +16,56 @@ function createContainer() { $container = createContainer(); $container->append("Hello, world!"); var_dump($container->innerHTML); +var_dump($container->outerHTML); $container = createContainer(); $container->append($dom->createComment("This is -a- comment")); var_dump($container->innerHTML); +var_dump($container->outerHTML); $container = createContainer(); // Note: intentionally typo'd to check whether the string matching against "xml" happens correctly // i.e. no bugs with prefix-matching only. $container->append($dom->createProcessingInstruction("xmll", "")); var_dump($container->innerHTML); +var_dump($container->outerHTML); $container = createContainer(); $container->append($dom->createProcessingInstruction("almostmalformed", ">?")); var_dump($container->innerHTML); +var_dump($container->outerHTML); $container = createContainer(); $element = $container->appendChild(createContainer()); $element->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns', 'http://example.com/'); var_dump($container->innerHTML); +var_dump($container->outerHTML); $container = createContainer(); $element = $container->appendChild(createContainer()); $element->setAttributeNS('urn:a', 'name', ''); $element->setAttributeNS('urn:b', 'name', ''); var_dump($container->innerHTML); +var_dump($container->outerHTML); $dom = DOM\XMLDocument::createFromFile(__DIR__ . '/../../book.xml'); var_dump($dom->documentElement->innerHTML); +var_dump($dom->documentElement->outerHTML); ?> --EXPECT-- string(13) "Hello, world!" +string(36) "Hello, world!" string(26) "" +string(49) "" string(9) "" +string(32) "" string(22) "??>" +string(45) "??>" string(12) "" +string(35) "" string(72) "" +string(95) "" string(167) " The Grapes of Wrath @@ -63,3 +76,13 @@ string(167) " John Steinbeck " +string(182) " + + The Grapes of Wrath + John Steinbeck + + + The Pearl + John Steinbeck + +" diff --git a/ext/dom/tests/modern/xml/Element_innerHTML_reading_errors.phpt b/ext/dom/tests/modern/xml/Element_innerOuterHTML_reading_errors.phpt similarity index 77% rename from ext/dom/tests/modern/xml/Element_innerHTML_reading_errors.phpt rename to ext/dom/tests/modern/xml/Element_innerOuterHTML_reading_errors.phpt index 04b9971186c4..585c9c5fd875 100644 --- a/ext/dom/tests/modern/xml/Element_innerHTML_reading_errors.phpt +++ b/ext/dom/tests/modern/xml/Element_innerOuterHTML_reading_errors.phpt @@ -1,5 +1,5 @@ --TEST-- -Test reading Element::$innerHTML on XML documents - error cases +Test reading Element::${inner,outer}HTML on XML documents - error cases --EXTENSIONS-- dom --FILE-- @@ -19,6 +19,11 @@ function test($container) { } catch (DOMException $e) { echo $e->getMessage(), "\n"; } + try { + var_dump($container->outerHTML); + } catch (DOMException $e) { + echo $e->getMessage(), "\n"; + } } $container = createContainer(); @@ -106,3 +111,18 @@ The resulting XML serialization is not well-formed The resulting XML serialization is not well-formed The resulting XML serialization is not well-formed The resulting XML serialization is not well-formed +The resulting XML serialization is not well-formed +The resulting XML serialization is not well-formed +The resulting XML serialization is not well-formed +The resulting XML serialization is not well-formed +The resulting XML serialization is not well-formed +The resulting XML serialization is not well-formed +The resulting XML serialization is not well-formed +The resulting XML serialization is not well-formed +The resulting XML serialization is not well-formed +The resulting XML serialization is not well-formed +The resulting XML serialization is not well-formed +The resulting XML serialization is not well-formed +The resulting XML serialization is not well-formed +The resulting XML serialization is not well-formed +The resulting XML serialization is not well-formed diff --git a/ext/dom/tests/modern/xml/Element_outerHTML_writing.phpt b/ext/dom/tests/modern/xml/Element_outerHTML_writing.phpt new file mode 100644 index 000000000000..f4bb3c8c4fce --- /dev/null +++ b/ext/dom/tests/modern/xml/Element_outerHTML_writing.phpt @@ -0,0 +1,35 @@ +--TEST-- +Test writing Element::$outerHTML on XML documents +--EXTENSIONS-- +dom +--FILE-- +"); +$dom->documentElement->firstChild->outerHTML = '

foo

bar

'; +echo $dom->saveXML(), "\n"; + +$dom->documentElement->firstChild->outerHTML = $dom->documentElement->firstChild->outerHTML; +$element = $dom->documentElement->firstChild->firstChild; +echo $dom->saveXML(), "\n"; + +$dom->documentElement->firstChild->outerHTML = 'tést'; +echo $dom->saveXML(), "\n"; + +var_dump($element->tagName); + +$fragment = $dom->createDocumentFragment(); +$fragment->appendChild($dom->createElement('p')); +$fragment->firstChild->outerHTML = 'bar'; +echo $dom->saveXML($fragment), "\n"; + +?> +--EXPECT-- + +

foo

bar

+ +

foo

bar

+ +tést +string(1) "p" +bar diff --git a/ext/dom/tests/modern/xml/Element_outerHTML_writing_errors.phpt b/ext/dom/tests/modern/xml/Element_outerHTML_writing_errors.phpt new file mode 100644 index 000000000000..f7602539acac --- /dev/null +++ b/ext/dom/tests/modern/xml/Element_outerHTML_writing_errors.phpt @@ -0,0 +1,25 @@ +--TEST-- +Test writing Element::$outerHTML on XML documents - error cases +--EXTENSIONS-- +dom +--FILE-- +'); +try { + $dom->documentElement->outerHTML = ''; +} catch (DOMException $e) { + echo $e->getMessage(), "\n"; +} + +$dom = Dom\XMLDocument::createFromString(''); +try { + $dom->documentElement->firstChild->outerHTML = ''; +} catch (DOMException $e) { + echo $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +Invalid Modification Error +XML fragment is not well-formed