From 201c15f7635c949132320c7e5067be88ee8a112e Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sat, 23 Mar 2024 13:54:51 +0100 Subject: [PATCH 1/7] Implement Dom\Document::$body getter --- ext/dom/dom_properties.h | 1 + ext/dom/html_document.c | 25 +++++ ext/dom/php_dom.c | 1 + ext/dom/php_dom.stub.php | 2 + ext/dom/php_dom_arginfo.h | 9 +- .../html/interactions/Document_body.phpt | 98 +++++++++++++++++++ ...should_retain_properties_and_owner_01.phpt | 4 +- ...should_retain_properties_and_owner_02.phpt | 4 +- ...ocument_implementation_createDocument.phpt | 4 +- .../tests/modern/xml/XMLDocument_debug.phpt | 4 +- .../xml/XMLDocument_fromEmptyDocument_02.phpt | 4 +- ...MLDocument_node_ownerDocument_for_XML.phpt | 4 +- 12 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 ext/dom/tests/modern/html/interactions/Document_body.phpt diff --git a/ext/dom/dom_properties.h b/ext/dom/dom_properties.h index fc8e530b01590..d5da955ebfead 100644 --- a/ext/dom/dom_properties.h +++ b/ext/dom/dom_properties.h @@ -62,6 +62,7 @@ zend_result dom_document_substitue_entities_write(dom_object *obj, zval *newval) /* html5 document properties */ zend_result dom_html_document_encoding_write(dom_object *obj, zval *retval); +zend_result dom_html_document_body_read(dom_object *obj, zval *retval); /* documenttype properties */ zend_result dom_documenttype_name_read(dom_object *obj, zval *retval); diff --git a/ext/dom/html_document.c b/ext/dom/html_document.c index 9fef8c25a8af6..74a1800bbdf57 100644 --- a/ext/dom/html_document.c +++ b/ext/dom/html_document.c @@ -1358,4 +1358,29 @@ zend_result dom_html_document_encoding_write(dom_object *obj, zval *newval) return SUCCESS; } +/* https://html.spec.whatwg.org/#dom-document-body */ +zend_result dom_html_document_body_read(dom_object *obj, zval *retval) +{ + DOM_PROP_NODE(const xmlDoc *, docp, obj); + + const xmlNode *root = xmlDocGetRootElement(docp); + if (root == NULL || !(php_dom_ns_is_fast(root, php_dom_ns_is_html_magic_token) && xmlStrEqual(root->name, BAD_CAST "html"))) { + ZVAL_NULL(retval); + return SUCCESS; + } + + xmlNodePtr cur = root->children; + while (cur != NULL) { + if (cur->type == XML_ELEMENT_NODE && php_dom_ns_is_fast(cur, php_dom_ns_is_html_magic_token) + && (xmlStrEqual(cur->name, BAD_CAST "body") || xmlStrEqual(cur->name, BAD_CAST "frameset"))) { + php_dom_create_object(cur, retval, obj); + return SUCCESS; + } + cur = cur->next; + } + + ZVAL_NULL(retval); + return SUCCESS; +} + #endif /* HAVE_LIBXML && HAVE_DOM */ diff --git a/ext/dom/php_dom.c b/ext/dom/php_dom.c index 2d21a891ca52a..7d0fda2918ded 100644 --- a/ext/dom/php_dom.c +++ b/ext/dom/php_dom.c @@ -851,6 +851,7 @@ PHP_MINIT_FUNCTION(dom) DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "firstElementChild", dom_parent_node_first_element_child_read, NULL); DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "lastElementChild", dom_parent_node_last_element_child_read, NULL); DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "childElementCount", dom_parent_node_child_element_count, NULL); + DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "body", dom_html_document_body_read, NULL); zend_hash_merge(&dom_abstract_base_document_prop_handlers, &dom_modern_node_prop_handlers, NULL, false); /* No need to register in &classes because this is an abstract class handler. */ diff --git a/ext/dom/php_dom.stub.php b/ext/dom/php_dom.stub.php index 50fced8c46691..34525d2158e1f 100644 --- a/ext/dom/php_dom.stub.php +++ b/ext/dom/php_dom.stub.php @@ -1580,6 +1580,8 @@ public function prepend(Node|string ...$nodes): void {} public function replaceChildren(Node|string ...$nodes): void {} public function importLegacyNode(\DOMNode $node, bool $deep = false): Node {} + + public ?Element $body; } final class HTMLDocument extends Document diff --git a/ext/dom/php_dom_arginfo.h b/ext/dom/php_dom_arginfo.h index a9a61a4df79cb..ce7188d39521f 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: c93643bad9675fddf31ca52f82f843218f208a5d */ + * Stub hash: 4d7d3a428304aa0544da0b1b57caa4e5948fa31b */ 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) @@ -3442,6 +3442,13 @@ static zend_class_entry *register_class_Dom_Document(zend_class_entry *class_ent zend_declare_typed_property(class_entry, property_childElementCount_name, &property_childElementCount_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); zend_string_release(property_childElementCount_name); + zval property_body_default_value; + ZVAL_UNDEF(&property_body_default_value); + zend_string *property_body_name = zend_string_init("body", sizeof("body") - 1, 1); + zend_string *property_body_class_Dom_Element = zend_string_init("Dom\\Element", sizeof("Dom\\Element")-1, 1); + zend_declare_typed_property(class_entry, property_body_name, &property_body_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_body_class_Dom_Element, 0, MAY_BE_NULL)); + zend_string_release(property_body_name); + return class_entry; } diff --git a/ext/dom/tests/modern/html/interactions/Document_body.phpt b/ext/dom/tests/modern/html/interactions/Document_body.phpt new file mode 100644 index 0000000000000..5010e6e168bdc --- /dev/null +++ b/ext/dom/tests/modern/html/interactions/Document_body.phpt @@ -0,0 +1,98 @@ +--TEST-- +Test Dom\Document::$body +--EXTENSIONS-- +dom +--FILE-- +foo

", LIBXML_NOERROR); +var_dump($dom->body?->nodeName); + +echo "--- After body removal ---\n"; + +$dom->body->remove(); +var_dump($dom->body?->nodeName); + +echo "--- body in no namespace ---\n"; + +$tmp = $dom->documentElement->appendChild($dom->createElementNS("", "body")); +var_dump($dom->body?->nodeName); +$tmp->remove(); + +echo "--- frameset in no namespace ---\n"; + +$tmp = $dom->documentElement->appendChild($dom->createElementNS("", "frameset")); +var_dump($dom->body?->nodeName); +$tmp->remove(); + +echo "--- body in right namespace ---\n"; + +$tmp = $dom->documentElement->appendChild($dom->createElementNS("http://www.w3.org/1999/xhtml", "body")); +var_dump($dom->body?->nodeName); +$tmp->remove(); + +echo "--- frameset in right namespace ---\n"; + +$tmp = $dom->documentElement->appendChild($dom->createElementNS("http://www.w3.org/1999/xhtml", "frameset")); +var_dump($dom->body?->nodeName); +$tmp->remove(); + +echo "--- prefixed body in right namespace ---\n"; + +$tmp = $dom->documentElement->appendChild($dom->createElementNS("http://www.w3.org/1999/xhtml", "prefix:body")); +var_dump($dom->body?->nodeName); +$tmp->remove(); + +echo "--- prefixed frameset in right namespace ---\n"; + +$tmp = $dom->documentElement->appendChild($dom->createElementNS("http://www.w3.org/1999/xhtml", "prefix:frameset")); +var_dump($dom->body?->nodeName); +$tmp->remove(); + +echo "--- multiple body-like elements in right namespace ---\n"; + +$tmp1 = $dom->documentElement->appendChild($dom->createElementNS("http://www.w3.org/1999/xhtml", "prefix1:body")); +var_dump($dom->body?->nodeName); +$tmp2 = $dom->documentElement->appendChild($dom->createElementNS("http://www.w3.org/1999/xhtml", "prefix2:frameset")); +var_dump($dom->body?->nodeName); +$tmp1->remove(); +var_dump($dom->body?->nodeName); +$tmp2->remove(); +var_dump($dom->body?->nodeName); + +echo "--- html element in no namespace ---\n"; + +$dom = Dom\XMLDocument::createFromString(<< + + +XML); +var_dump($dom->body); + +?> +--EXPECT-- +--- From parsing --- +string(4) "BODY" +--- After body removal --- +NULL +--- body in no namespace --- +NULL +--- frameset in no namespace --- +NULL +--- body in right namespace --- +string(4) "BODY" +--- frameset in right namespace --- +string(8) "FRAMESET" +--- prefixed body in right namespace --- +string(11) "PREFIX:BODY" +--- prefixed frameset in right namespace --- +string(15) "PREFIX:FRAMESET" +--- multiple body-like elements in right namespace --- +string(12) "PREFIX1:BODY" +string(12) "PREFIX1:BODY" +string(16) "PREFIX2:FRAMESET" +NULL +--- html element in no namespace --- +NULL diff --git a/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_01.phpt b/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_01.phpt index 2931aebab28f5..a82d882cd366c 100644 --- a/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_01.phpt +++ b/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_01.phpt @@ -23,7 +23,7 @@ var_dump(get_class($dom->getElementsByTagName("p")->item(0))); ?> --EXPECT-- -object(Dom\HTMLDocument)#1 (25) { +object(Dom\HTMLDocument)#1 (26) { ["implementation"]=> string(22) "(object value omitted)" ["URL"]=> @@ -46,6 +46,8 @@ object(Dom\HTMLDocument)#1 (25) { string(22) "(object value omitted)" ["childElementCount"]=> int(1) + ["body"]=> + string(22) "(object value omitted)" ["nodeType"]=> int(13) ["nodeName"]=> diff --git a/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_02.phpt b/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_02.phpt index c356cf7ba9215..67565b13fbc32 100644 --- a/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_02.phpt +++ b/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_02.phpt @@ -23,7 +23,7 @@ var_dump(get_class($dom->getElementsByTagName("p")->item(0))); ?> --EXPECT-- -object(Dom\HTMLDocument)#1 (25) { +object(Dom\HTMLDocument)#1 (26) { ["implementation"]=> string(22) "(object value omitted)" ["URL"]=> @@ -46,6 +46,8 @@ object(Dom\HTMLDocument)#1 (25) { string(22) "(object value omitted)" ["childElementCount"]=> int(1) + ["body"]=> + string(22) "(object value omitted)" ["nodeType"]=> int(13) ["nodeName"]=> diff --git a/ext/dom/tests/modern/spec/Document_implementation_createDocument.phpt b/ext/dom/tests/modern/spec/Document_implementation_createDocument.phpt index 5384cf6331f76..57313e4260247 100644 --- a/ext/dom/tests/modern/spec/Document_implementation_createDocument.phpt +++ b/ext/dom/tests/modern/spec/Document_implementation_createDocument.phpt @@ -37,7 +37,7 @@ echo $dom->implementation->createDocument(null, "", $dtd)->saveXml(), "\n"; ?> --EXPECT-- --- (null, "") --- -object(Dom\XMLDocument)#3 (29) { +object(Dom\XMLDocument)#3 (30) { ["xmlEncoding"]=> string(5) "UTF-8" ["xmlStandalone"]=> @@ -68,6 +68,8 @@ object(Dom\XMLDocument)#3 (29) { NULL ["childElementCount"]=> int(0) + ["body"]=> + NULL ["nodeType"]=> int(9) ["nodeName"]=> diff --git a/ext/dom/tests/modern/xml/XMLDocument_debug.phpt b/ext/dom/tests/modern/xml/XMLDocument_debug.phpt index c9ae2afa95fdf..2d09e93e147eb 100644 --- a/ext/dom/tests/modern/xml/XMLDocument_debug.phpt +++ b/ext/dom/tests/modern/xml/XMLDocument_debug.phpt @@ -10,7 +10,7 @@ var_dump($dom); ?> --EXPECT-- -object(Dom\XMLDocument)#1 (29) { +object(Dom\XMLDocument)#1 (30) { ["xmlEncoding"]=> string(5) "UTF-8" ["xmlStandalone"]=> @@ -41,6 +41,8 @@ object(Dom\XMLDocument)#1 (29) { NULL ["childElementCount"]=> int(0) + ["body"]=> + NULL ["nodeType"]=> int(9) ["nodeName"]=> diff --git a/ext/dom/tests/modern/xml/XMLDocument_fromEmptyDocument_02.phpt b/ext/dom/tests/modern/xml/XMLDocument_fromEmptyDocument_02.phpt index a804394bc8441..8b8fd9d2003d6 100644 --- a/ext/dom/tests/modern/xml/XMLDocument_fromEmptyDocument_02.phpt +++ b/ext/dom/tests/modern/xml/XMLDocument_fromEmptyDocument_02.phpt @@ -10,7 +10,7 @@ var_dump($dom); ?> --EXPECT-- -object(Dom\XMLDocument)#1 (29) { +object(Dom\XMLDocument)#1 (30) { ["xmlEncoding"]=> string(5) "UTF-8" ["xmlStandalone"]=> @@ -41,6 +41,8 @@ object(Dom\XMLDocument)#1 (29) { NULL ["childElementCount"]=> int(0) + ["body"]=> + NULL ["nodeType"]=> int(9) ["nodeName"]=> diff --git a/ext/dom/tests/modern/xml/XMLDocument_node_ownerDocument_for_XML.phpt b/ext/dom/tests/modern/xml/XMLDocument_node_ownerDocument_for_XML.phpt index 946f9495114a7..ee2d5b0116b95 100644 --- a/ext/dom/tests/modern/xml/XMLDocument_node_ownerDocument_for_XML.phpt +++ b/ext/dom/tests/modern/xml/XMLDocument_node_ownerDocument_for_XML.phpt @@ -13,7 +13,7 @@ var_dump($element->ownerDocument); ?> --EXPECTF-- -object(Dom\XMLDocument)#1 (29) { +object(Dom\XMLDocument)#1 (30) { ["xmlEncoding"]=> string(5) "UTF-8" ["xmlStandalone"]=> @@ -44,6 +44,8 @@ object(Dom\XMLDocument)#1 (29) { string(22) "(object value omitted)" ["childElementCount"]=> int(1) + ["body"]=> + NULL ["nodeType"]=> int(9) ["nodeName"]=> From 4fc905fb9b85e89d70e8b7a860205c6f75f33141 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sat, 23 Mar 2024 14:02:49 +0100 Subject: [PATCH 2/7] Implement Dom\Document::$head --- ext/dom/dom_properties.h | 1 + ext/dom/html_document.c | 28 ++++++- ext/dom/php_dom.c | 1 + ext/dom/php_dom.stub.php | 2 + ext/dom/php_dom_arginfo.h | 9 ++- .../html/interactions/Document_head.phpt | 74 +++++++++++++++++++ ...should_retain_properties_and_owner_01.phpt | 4 +- ...should_retain_properties_and_owner_02.phpt | 4 +- ...ocument_implementation_createDocument.phpt | 4 +- .../tests/modern/xml/XMLDocument_debug.phpt | 4 +- .../xml/XMLDocument_fromEmptyDocument_02.phpt | 4 +- ...MLDocument_node_ownerDocument_for_XML.phpt | 4 +- 12 files changed, 128 insertions(+), 11 deletions(-) create mode 100644 ext/dom/tests/modern/html/interactions/Document_head.phpt diff --git a/ext/dom/dom_properties.h b/ext/dom/dom_properties.h index d5da955ebfead..5ab6fedfe59ea 100644 --- a/ext/dom/dom_properties.h +++ b/ext/dom/dom_properties.h @@ -63,6 +63,7 @@ zend_result dom_document_substitue_entities_write(dom_object *obj, zval *newval) /* html5 document properties */ zend_result dom_html_document_encoding_write(dom_object *obj, zval *retval); zend_result dom_html_document_body_read(dom_object *obj, zval *retval); +zend_result dom_html_document_head_read(dom_object *obj, zval *retval); /* documenttype properties */ zend_result dom_documenttype_name_read(dom_object *obj, zval *retval); diff --git a/ext/dom/html_document.c b/ext/dom/html_document.c index 74a1800bbdf57..5cfb75fecdd79 100644 --- a/ext/dom/html_document.c +++ b/ext/dom/html_document.c @@ -1358,8 +1358,7 @@ zend_result dom_html_document_encoding_write(dom_object *obj, zval *newval) return SUCCESS; } -/* https://html.spec.whatwg.org/#dom-document-body */ -zend_result dom_html_document_body_read(dom_object *obj, zval *retval) +zend_result dom_html_document_element_read_helper(dom_object *obj, zval *retval, bool (*accept)(const xmlChar *)) { DOM_PROP_NODE(const xmlDoc *, docp, obj); @@ -1371,8 +1370,7 @@ zend_result dom_html_document_body_read(dom_object *obj, zval *retval) xmlNodePtr cur = root->children; while (cur != NULL) { - if (cur->type == XML_ELEMENT_NODE && php_dom_ns_is_fast(cur, php_dom_ns_is_html_magic_token) - && (xmlStrEqual(cur->name, BAD_CAST "body") || xmlStrEqual(cur->name, BAD_CAST "frameset"))) { + if (cur->type == XML_ELEMENT_NODE && php_dom_ns_is_fast(cur, php_dom_ns_is_html_magic_token) && accept(cur->name)) { php_dom_create_object(cur, retval, obj); return SUCCESS; } @@ -1383,4 +1381,26 @@ zend_result dom_html_document_body_read(dom_object *obj, zval *retval) return SUCCESS; } +static bool dom_accept_body_name(const xmlChar *name) +{ + return xmlStrEqual(name, BAD_CAST "body") || xmlStrEqual(name, BAD_CAST "frameset"); +} + +static bool dom_accept_head_name(const xmlChar *name) +{ + return xmlStrEqual(name, BAD_CAST "head"); +} + +/* https://html.spec.whatwg.org/#dom-document-body */ +zend_result dom_html_document_body_read(dom_object *obj, zval *retval) +{ + return dom_html_document_element_read_helper(obj, retval, dom_accept_body_name); +} + +/* https://html.spec.whatwg.org/#dom-document-head */ +zend_result dom_html_document_head_read(dom_object *obj, zval *retval) +{ + return dom_html_document_element_read_helper(obj, retval, dom_accept_head_name); +} + #endif /* HAVE_LIBXML && HAVE_DOM */ diff --git a/ext/dom/php_dom.c b/ext/dom/php_dom.c index 7d0fda2918ded..b08dc935a9a01 100644 --- a/ext/dom/php_dom.c +++ b/ext/dom/php_dom.c @@ -852,6 +852,7 @@ PHP_MINIT_FUNCTION(dom) DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "lastElementChild", dom_parent_node_last_element_child_read, NULL); DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "childElementCount", dom_parent_node_child_element_count, NULL); DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "body", dom_html_document_body_read, NULL); + DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "head", dom_html_document_head_read, NULL); zend_hash_merge(&dom_abstract_base_document_prop_handlers, &dom_modern_node_prop_handlers, NULL, false); /* No need to register in &classes because this is an abstract class handler. */ diff --git a/ext/dom/php_dom.stub.php b/ext/dom/php_dom.stub.php index 34525d2158e1f..a21b818b89994 100644 --- a/ext/dom/php_dom.stub.php +++ b/ext/dom/php_dom.stub.php @@ -1582,6 +1582,8 @@ public function replaceChildren(Node|string ...$nodes): void {} public function importLegacyNode(\DOMNode $node, bool $deep = false): Node {} public ?Element $body; + /** @readonly */ + public ?Element $head; } final class HTMLDocument extends Document diff --git a/ext/dom/php_dom_arginfo.h b/ext/dom/php_dom_arginfo.h index ce7188d39521f..b364952687e7f 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: 4d7d3a428304aa0544da0b1b57caa4e5948fa31b */ + * Stub hash: 0795d4e52f62ab33df92cfbdd5178223fbfc3eeb */ 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) @@ -3449,6 +3449,13 @@ static zend_class_entry *register_class_Dom_Document(zend_class_entry *class_ent zend_declare_typed_property(class_entry, property_body_name, &property_body_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_body_class_Dom_Element, 0, MAY_BE_NULL)); zend_string_release(property_body_name); + zval property_head_default_value; + ZVAL_UNDEF(&property_head_default_value); + zend_string *property_head_name = zend_string_init("head", sizeof("head") - 1, 1); + zend_string *property_head_class_Dom_Element = zend_string_init("Dom\\Element", sizeof("Dom\\Element")-1, 1); + zend_declare_typed_property(class_entry, property_head_name, &property_head_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_head_class_Dom_Element, 0, MAY_BE_NULL)); + zend_string_release(property_head_name); + return class_entry; } diff --git a/ext/dom/tests/modern/html/interactions/Document_head.phpt b/ext/dom/tests/modern/html/interactions/Document_head.phpt new file mode 100644 index 0000000000000..cc34945951405 --- /dev/null +++ b/ext/dom/tests/modern/html/interactions/Document_head.phpt @@ -0,0 +1,74 @@ +--TEST-- +Test Dom\Document::$head +--EXTENSIONS-- +dom +--FILE-- +foo

", LIBXML_NOERROR); +var_dump($dom->head?->nodeName); + +echo "--- After head removal ---\n"; + +$dom->head->remove(); +var_dump($dom->head?->nodeName); + +echo "--- head in no namespace ---\n"; + +$tmp = $dom->documentElement->appendChild($dom->createElementNS("", "head")); +var_dump($dom->head?->nodeName); +$tmp->remove(); + +echo "--- head in right namespace ---\n"; + +$tmp = $dom->documentElement->appendChild($dom->createElementNS("http://www.w3.org/1999/xhtml", "head")); +var_dump($dom->head?->nodeName); +$tmp->remove(); + +echo "--- prefixed head in right namespace ---\n"; + +$tmp = $dom->documentElement->appendChild($dom->createElementNS("http://www.w3.org/1999/xhtml", "prefix:head")); +var_dump($dom->head?->nodeName); +$tmp->remove(); + +echo "--- multiple head elements in right namespace ---\n"; + +$tmp1 = $dom->documentElement->appendChild($dom->createElementNS("http://www.w3.org/1999/xhtml", "prefix1:head")); +var_dump($dom->head?->nodeName); +$tmp2 = $dom->documentElement->appendChild($dom->createElementNS("http://www.w3.org/1999/xhtml", "prefix2:head")); +var_dump($dom->head?->nodeName); +$tmp1->remove(); +var_dump($dom->head?->nodeName); +$tmp2->remove(); +var_dump($dom->head?->nodeName); + +echo "--- html element in no namespace ---\n"; + +$dom = Dom\XMLDocument::createFromString(<< + + +XML); +var_dump($dom->head); + +?> +--EXPECT-- +--- From parsing --- +string(4) "HEAD" +--- After head removal --- +NULL +--- head in no namespace --- +NULL +--- head in right namespace --- +string(4) "HEAD" +--- prefixed head in right namespace --- +string(11) "PREFIX:HEAD" +--- multiple head elements in right namespace --- +string(12) "PREFIX1:HEAD" +string(12) "PREFIX1:HEAD" +string(12) "PREFIX2:HEAD" +NULL +--- html element in no namespace --- +NULL diff --git a/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_01.phpt b/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_01.phpt index a82d882cd366c..593dae8e1f27e 100644 --- a/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_01.phpt +++ b/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_01.phpt @@ -23,7 +23,7 @@ var_dump(get_class($dom->getElementsByTagName("p")->item(0))); ?> --EXPECT-- -object(Dom\HTMLDocument)#1 (26) { +object(Dom\HTMLDocument)#1 (27) { ["implementation"]=> string(22) "(object value omitted)" ["URL"]=> @@ -48,6 +48,8 @@ object(Dom\HTMLDocument)#1 (26) { int(1) ["body"]=> string(22) "(object value omitted)" + ["head"]=> + string(22) "(object value omitted)" ["nodeType"]=> int(13) ["nodeName"]=> diff --git a/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_02.phpt b/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_02.phpt index 67565b13fbc32..b26205647786d 100644 --- a/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_02.phpt +++ b/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_02.phpt @@ -23,7 +23,7 @@ var_dump(get_class($dom->getElementsByTagName("p")->item(0))); ?> --EXPECT-- -object(Dom\HTMLDocument)#1 (26) { +object(Dom\HTMLDocument)#1 (27) { ["implementation"]=> string(22) "(object value omitted)" ["URL"]=> @@ -48,6 +48,8 @@ object(Dom\HTMLDocument)#1 (26) { int(1) ["body"]=> string(22) "(object value omitted)" + ["head"]=> + string(22) "(object value omitted)" ["nodeType"]=> int(13) ["nodeName"]=> diff --git a/ext/dom/tests/modern/spec/Document_implementation_createDocument.phpt b/ext/dom/tests/modern/spec/Document_implementation_createDocument.phpt index 57313e4260247..37d73c5d40a7f 100644 --- a/ext/dom/tests/modern/spec/Document_implementation_createDocument.phpt +++ b/ext/dom/tests/modern/spec/Document_implementation_createDocument.phpt @@ -37,7 +37,7 @@ echo $dom->implementation->createDocument(null, "", $dtd)->saveXml(), "\n"; ?> --EXPECT-- --- (null, "") --- -object(Dom\XMLDocument)#3 (30) { +object(Dom\XMLDocument)#3 (31) { ["xmlEncoding"]=> string(5) "UTF-8" ["xmlStandalone"]=> @@ -70,6 +70,8 @@ object(Dom\XMLDocument)#3 (30) { int(0) ["body"]=> NULL + ["head"]=> + NULL ["nodeType"]=> int(9) ["nodeName"]=> diff --git a/ext/dom/tests/modern/xml/XMLDocument_debug.phpt b/ext/dom/tests/modern/xml/XMLDocument_debug.phpt index 2d09e93e147eb..a0f6a528389b6 100644 --- a/ext/dom/tests/modern/xml/XMLDocument_debug.phpt +++ b/ext/dom/tests/modern/xml/XMLDocument_debug.phpt @@ -10,7 +10,7 @@ var_dump($dom); ?> --EXPECT-- -object(Dom\XMLDocument)#1 (30) { +object(Dom\XMLDocument)#1 (31) { ["xmlEncoding"]=> string(5) "UTF-8" ["xmlStandalone"]=> @@ -43,6 +43,8 @@ object(Dom\XMLDocument)#1 (30) { int(0) ["body"]=> NULL + ["head"]=> + NULL ["nodeType"]=> int(9) ["nodeName"]=> diff --git a/ext/dom/tests/modern/xml/XMLDocument_fromEmptyDocument_02.phpt b/ext/dom/tests/modern/xml/XMLDocument_fromEmptyDocument_02.phpt index 8b8fd9d2003d6..b8a87e5ba4ea6 100644 --- a/ext/dom/tests/modern/xml/XMLDocument_fromEmptyDocument_02.phpt +++ b/ext/dom/tests/modern/xml/XMLDocument_fromEmptyDocument_02.phpt @@ -10,7 +10,7 @@ var_dump($dom); ?> --EXPECT-- -object(Dom\XMLDocument)#1 (30) { +object(Dom\XMLDocument)#1 (31) { ["xmlEncoding"]=> string(5) "UTF-8" ["xmlStandalone"]=> @@ -43,6 +43,8 @@ object(Dom\XMLDocument)#1 (30) { int(0) ["body"]=> NULL + ["head"]=> + NULL ["nodeType"]=> int(9) ["nodeName"]=> diff --git a/ext/dom/tests/modern/xml/XMLDocument_node_ownerDocument_for_XML.phpt b/ext/dom/tests/modern/xml/XMLDocument_node_ownerDocument_for_XML.phpt index ee2d5b0116b95..4f3308343e994 100644 --- a/ext/dom/tests/modern/xml/XMLDocument_node_ownerDocument_for_XML.phpt +++ b/ext/dom/tests/modern/xml/XMLDocument_node_ownerDocument_for_XML.phpt @@ -13,7 +13,7 @@ var_dump($element->ownerDocument); ?> --EXPECTF-- -object(Dom\XMLDocument)#1 (30) { +object(Dom\XMLDocument)#1 (31) { ["xmlEncoding"]=> string(5) "UTF-8" ["xmlStandalone"]=> @@ -46,6 +46,8 @@ object(Dom\XMLDocument)#1 (30) { int(1) ["body"]=> NULL + ["head"]=> + NULL ["nodeType"]=> int(9) ["nodeName"]=> From edbdaaa9a46f2a48c7219e52e449d1587828350a Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sat, 23 Mar 2024 14:59:31 +0100 Subject: [PATCH 3/7] Implement Dom\Document::$body setter --- ext/dom/dom_properties.h | 1 + ext/dom/html_document.c | 73 ++++++++++++++++--- ext/dom/php_dom.c | 2 +- ...nt_body.phpt => Document_body_getter.phpt} | 2 +- .../interactions/Document_body_setter.phpt | 48 ++++++++++++ .../Document_body_setter_errors.phpt | 61 ++++++++++++++++ 6 files changed, 176 insertions(+), 11 deletions(-) rename ext/dom/tests/modern/html/interactions/{Document_body.phpt => Document_body_getter.phpt} (98%) create mode 100644 ext/dom/tests/modern/html/interactions/Document_body_setter.phpt create mode 100644 ext/dom/tests/modern/html/interactions/Document_body_setter_errors.phpt diff --git a/ext/dom/dom_properties.h b/ext/dom/dom_properties.h index 5ab6fedfe59ea..1f8d1e44f34e2 100644 --- a/ext/dom/dom_properties.h +++ b/ext/dom/dom_properties.h @@ -63,6 +63,7 @@ zend_result dom_document_substitue_entities_write(dom_object *obj, zval *newval) /* html5 document properties */ zend_result dom_html_document_encoding_write(dom_object *obj, zval *retval); zend_result dom_html_document_body_read(dom_object *obj, zval *retval); +zend_result dom_html_document_body_write(dom_object *obj, zval *newval); zend_result dom_html_document_head_read(dom_object *obj, zval *retval); /* documenttype properties */ diff --git a/ext/dom/html_document.c b/ext/dom/html_document.c index 5cfb75fecdd79..e4fdf248a8905 100644 --- a/ext/dom/html_document.c +++ b/ext/dom/html_document.c @@ -1358,26 +1358,35 @@ zend_result dom_html_document_encoding_write(dom_object *obj, zval *newval) return SUCCESS; } -zend_result dom_html_document_element_read_helper(dom_object *obj, zval *retval, bool (*accept)(const xmlChar *)) +static const xmlNode *dom_html_document_element_read_raw(const xmlDoc *docp, bool (*accept)(const xmlChar *)) { - DOM_PROP_NODE(const xmlDoc *, docp, obj); - const xmlNode *root = xmlDocGetRootElement(docp); if (root == NULL || !(php_dom_ns_is_fast(root, php_dom_ns_is_html_magic_token) && xmlStrEqual(root->name, BAD_CAST "html"))) { - ZVAL_NULL(retval); - return SUCCESS; + return NULL; } - xmlNodePtr cur = root->children; + const xmlNode *cur = root->children; while (cur != NULL) { if (cur->type == XML_ELEMENT_NODE && php_dom_ns_is_fast(cur, php_dom_ns_is_html_magic_token) && accept(cur->name)) { - php_dom_create_object(cur, retval, obj); - return SUCCESS; + return cur; } cur = cur->next; } - ZVAL_NULL(retval); + return NULL; +} + +zend_result dom_html_document_element_read_helper(dom_object *obj, zval *retval, bool (*accept)(const xmlChar *)) +{ + DOM_PROP_NODE(const xmlDoc *, docp, obj); + + const xmlNode *element = dom_html_document_element_read_raw(docp, accept); + if (element == NULL) { + ZVAL_NULL(retval); + } else { + php_dom_create_object((xmlNodePtr) element, retval, obj); + } + return SUCCESS; } @@ -1403,4 +1412,50 @@ zend_result dom_html_document_head_read(dom_object *obj, zval *retval) return dom_html_document_element_read_helper(obj, retval, dom_accept_head_name); } +/* https://html.spec.whatwg.org/#dom-document-body */ +zend_result dom_html_document_body_write(dom_object *obj, zval *newval) +{ + DOM_PROP_NODE(xmlDocPtr, docp, obj); + + /* 1. If the new value is not a body or frameset element, then throw a "HierarchyRequestError" DOMException. */ + if (Z_TYPE_P(newval) != IS_NULL) { + dom_object *newval_intern = Z_DOMOBJ_P(newval); + if (newval_intern->ptr != NULL) { + xmlNodePtr newval_node = ((php_libxml_node_ptr *) newval_intern->ptr)->node; + if (php_dom_ns_is_fast(newval_node, php_dom_ns_is_html_magic_token) && dom_accept_body_name(newval_node->name)) { + /* 2. If the new value is the same as the body element, return. */ + const xmlNode *current_body_element = dom_html_document_element_read_raw(docp, dom_accept_body_name); + if (current_body_element == newval_node) { + return SUCCESS; + } + + /* 3. If the body element is not null, then replace the body element with the new value within the body element's parent and return. */ + if (current_body_element != NULL) { + php_dom_adopt_node(newval_node, obj, docp); + xmlNodePtr old = xmlReplaceNode((xmlNodePtr) current_body_element, newval_node); + if (old != NULL && old->_private == NULL) { + php_libxml_node_free_resource(old); + } + return SUCCESS; + } + + /* 4. If there is no document element, throw a "HierarchyRequestError" DOMException. */ + xmlNodePtr root = xmlDocGetRootElement(docp); + if (root == NULL) { + php_dom_throw_error_with_message(HIERARCHY_REQUEST_ERR, "A body can only be set if there is a document element", true); + return FAILURE; + } + + /* 5. Append the new value to the document element. */ + php_dom_adopt_node(newval_node, obj, docp); + xmlAddChild(root, newval_node); + return SUCCESS; + } + } + } + + php_dom_throw_error_with_message(HIERARCHY_REQUEST_ERR, "The new body must either be a body or a frameset tag", true); + return FAILURE; +} + #endif /* HAVE_LIBXML && HAVE_DOM */ diff --git a/ext/dom/php_dom.c b/ext/dom/php_dom.c index b08dc935a9a01..3c26afd77800d 100644 --- a/ext/dom/php_dom.c +++ b/ext/dom/php_dom.c @@ -851,7 +851,7 @@ PHP_MINIT_FUNCTION(dom) DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "firstElementChild", dom_parent_node_first_element_child_read, NULL); DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "lastElementChild", dom_parent_node_last_element_child_read, NULL); DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "childElementCount", dom_parent_node_child_element_count, NULL); - DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "body", dom_html_document_body_read, NULL); + DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "body", dom_html_document_body_read, dom_html_document_body_write); DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "head", dom_html_document_head_read, NULL); zend_hash_merge(&dom_abstract_base_document_prop_handlers, &dom_modern_node_prop_handlers, NULL, false); /* No need to register in &classes because this is an abstract class handler. */ diff --git a/ext/dom/tests/modern/html/interactions/Document_body.phpt b/ext/dom/tests/modern/html/interactions/Document_body_getter.phpt similarity index 98% rename from ext/dom/tests/modern/html/interactions/Document_body.phpt rename to ext/dom/tests/modern/html/interactions/Document_body_getter.phpt index 5010e6e168bdc..79201ab3cf3b9 100644 --- a/ext/dom/tests/modern/html/interactions/Document_body.phpt +++ b/ext/dom/tests/modern/html/interactions/Document_body_getter.phpt @@ -1,5 +1,5 @@ --TEST-- -Test Dom\Document::$body +Test Dom\Document::$body getter --EXTENSIONS-- dom --FILE-- diff --git a/ext/dom/tests/modern/html/interactions/Document_body_setter.phpt b/ext/dom/tests/modern/html/interactions/Document_body_setter.phpt new file mode 100644 index 0000000000000..87cbb9b9c9e6e --- /dev/null +++ b/ext/dom/tests/modern/html/interactions/Document_body_setter.phpt @@ -0,0 +1,48 @@ +--TEST-- +Test DOM\Document::$body setter +--EXTENSIONS-- +dom +--FILE-- +foo

', LIBXML_NOERROR); +$dom->body = $dom->body; +var_dump($dom->body?->nodeName); + +echo "--- Add body when there is no body yet ---\n"; +$dom = DOM\HTMLDocument::createFromString('

foo

', LIBXML_NOERROR); +$dom->body->remove(); +$dom->body = $dom->createElementNS("http://www.w3.org/1999/xhtml", "prefix:body"); +var_dump($dom->body?->nodeName); + +echo "--- Replace old body with new body ---\n"; +$dom = DOM\HTMLDocument::createFromString('

foo

', LIBXML_NOERROR); +$dom->body = $dom->createElementNS("http://www.w3.org/1999/xhtml", "prefix:body"); +var_dump($dom->body?->nodeName); + +echo "--- Replace old body with new body, while still having a reference to the old body ---\n"; +$dom = DOM\HTMLDocument::createFromString('

foo

', LIBXML_NOERROR); +$old = $dom->body; +$dom->body = $dom->createElementNS("http://www.w3.org/1999/xhtml", "prefix:body"); +var_dump($dom->body?->nodeName); +var_dump($old->nodeName); + +echo "--- Special note from the DOM spec ---\n"; +$dom = DOM\XMLDocument::createFromString(''); +$dom->body = $dom->createElementNS("http://www.w3.org/1999/xhtml", "body"); +var_dump($dom->body?->nodeName); + +?> +--EXPECT-- +--- Replace body with itself --- +string(4) "BODY" +--- Add body when there is no body yet --- +string(11) "PREFIX:BODY" +--- Replace old body with new body --- +string(11) "PREFIX:BODY" +--- Replace old body with new body, while still having a reference to the old body --- +string(11) "PREFIX:BODY" +string(4) "BODY" +--- Special note from the DOM spec --- +NULL diff --git a/ext/dom/tests/modern/html/interactions/Document_body_setter_errors.phpt b/ext/dom/tests/modern/html/interactions/Document_body_setter_errors.phpt new file mode 100644 index 0000000000000..9913dbb12ae91 --- /dev/null +++ b/ext/dom/tests/modern/html/interactions/Document_body_setter_errors.phpt @@ -0,0 +1,61 @@ +--TEST-- +Test DOM\Document::$body setter errors +--EXTENSIONS-- +dom +--FILE-- +foo

', LIBXML_NOERROR); + var_dump($dom->body?->nodeName); + try { + $dom->body = $cb($dom); + } catch (DOMException $e) { + echo $e->getMessage(), "\n"; + } + var_dump($dom->body?->nodeName); +} + +echo "--- Set body to NULL ---\n"; +testNormalReplace(fn ($dom) => NULL); + +echo "--- Wrong element tag in right namespace ---\n"; +testNormalReplace(fn ($dom) => $dom->createElementNS("http://www.w3.org/1999/xhtml", "foo")); + +echo "--- Right element tag in wrong namespace ---\n"; +testNormalReplace(fn ($dom) => $dom->createElementNS("urn:a", "body")); + +echo "--- Right element tag in no namespace ---\n"; +testNormalReplace(fn ($dom) => $dom->createElementNS("", "frameset")); + +echo "--- Set body without document element ---\n"; +$dom = DOM\XMLDocument::createEmpty(); +try { + $dom->body = $dom->createElementNS("http://www.w3.org/1999/xhtml", "body"); +} catch (DOMException $e) { + echo $e->getMessage(), "\n"; +} +var_dump($dom->body?->nodeName); + +?> +--EXPECT-- +--- Set body to NULL --- +string(4) "BODY" +The new body must either be a body or a frameset tag +string(4) "BODY" +--- Wrong element tag in right namespace --- +string(4) "BODY" +The new body must either be a body or a frameset tag +string(4) "BODY" +--- Right element tag in wrong namespace --- +string(4) "BODY" +The new body must either be a body or a frameset tag +string(4) "BODY" +--- Right element tag in no namespace --- +string(4) "BODY" +The new body must either be a body or a frameset tag +string(4) "BODY" +--- Set body without document element --- +A body can only be set if there is a document element +NULL From 93b21c506bb016fc752255da3cd4d814daba318c Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sun, 7 Apr 2024 14:24:01 +0200 Subject: [PATCH 4/7] Implement Dom\Document::$title getter --- ext/dom/config.m4 | 2 +- ext/dom/config.w32 | 2 +- ext/dom/dom_properties.h | 1 + ext/dom/html_document.c | 79 +++++++++++++++++++ ext/dom/infra.c | 77 ++++++++++++++++++ ext/dom/infra.h | 26 ++++++ ext/dom/php_dom.c | 1 + ext/dom/php_dom.stub.php | 1 + ext/dom/php_dom_arginfo.h | 8 +- .../modern/common/Document_title_getter.phpt | 79 +++++++++++++++++++ ...should_retain_properties_and_owner_01.phpt | 4 +- ...should_retain_properties_and_owner_02.phpt | 4 +- ...ocument_implementation_createDocument.phpt | 4 +- .../tests/modern/xml/XMLDocument_debug.phpt | 4 +- .../xml/XMLDocument_fromEmptyDocument_02.phpt | 4 +- ...MLDocument_node_ownerDocument_for_XML.phpt | 4 +- 16 files changed, 291 insertions(+), 9 deletions(-) create mode 100644 ext/dom/infra.c create mode 100644 ext/dom/infra.h create mode 100644 ext/dom/tests/modern/common/Document_title_getter.phpt diff --git a/ext/dom/config.m4 b/ext/dom/config.m4 index 4dcde6105a583..0db013b8689d1 100644 --- a/ext/dom/config.m4 +++ b/ext/dom/config.m4 @@ -25,7 +25,7 @@ if test "$PHP_DOM" != "no"; then $LEXBOR_DIR/selectors/selectors.c \ $LEXBOR_DIR/ns/ns.c \ $LEXBOR_DIR/tag/tag.c" - PHP_NEW_EXTENSION(dom, [php_dom.c attr.c document.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 \ domexception.c parentnode.c \ processinginstruction.c cdatasection.c \ diff --git a/ext/dom/config.w32 b/ext/dom/config.w32 index 1a5d33bf7ca4f..02a7e0a9409d1 100644 --- a/ext/dom/config.w32 +++ b/ext/dom/config.w32 @@ -7,7 +7,7 @@ if (PHP_DOM == "yes") { ADD_EXTENSION_DEP('dom', 'libxml') && CHECK_HEADER_ADD_INCLUDE("libxml/parser.h", "CFLAGS_DOM", PHP_PHP_BUILD + "\\include\\libxml2") ) { - EXTENSION("dom", "php_dom.c attr.c document.c \ + 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 \ domexception.c parentnode.c processinginstruction.c \ cdatasection.c documentfragment.c domimplementation.c element.c \ diff --git a/ext/dom/dom_properties.h b/ext/dom/dom_properties.h index 1f8d1e44f34e2..039f7fd260e1c 100644 --- a/ext/dom/dom_properties.h +++ b/ext/dom/dom_properties.h @@ -65,6 +65,7 @@ zend_result dom_html_document_encoding_write(dom_object *obj, zval *retval); zend_result dom_html_document_body_read(dom_object *obj, zval *retval); zend_result dom_html_document_body_write(dom_object *obj, zval *newval); zend_result dom_html_document_head_read(dom_object *obj, zval *retval); +zend_result dom_html_document_title_read(dom_object *obj, zval *retval); /* documenttype properties */ zend_result dom_documenttype_name_read(dom_object *obj, zval *retval); diff --git a/ext/dom/html_document.c b/ext/dom/html_document.c index e4fdf248a8905..d487d04b7fbbe 100644 --- a/ext/dom/html_document.c +++ b/ext/dom/html_document.c @@ -21,6 +21,7 @@ #include "php.h" #if defined(HAVE_LIBXML) && defined(HAVE_DOM) #include "php_dom.h" +#include "infra.h" #include "html5_parser.h" #include "html5_serializer.h" #include "namespace_compat.h" @@ -1458,4 +1459,82 @@ zend_result dom_html_document_body_write(dom_object *obj, zval *newval) return FAILURE; } +/* https://dom.spec.whatwg.org/#concept-child-text-content */ +static zend_string *dom_get_child_text_content(const xmlNode *node) +{ + smart_str content = {0}; + + const xmlNode *text = node->children; + while (text != NULL) { + if (text->type == XML_TEXT_NODE || text->type == XML_CDATA_SECTION_NODE) { + smart_str_appends(&content, (const char *) text->content); + } + text = text->next; + } + + return smart_str_extract(&content); +} + +/* https://html.spec.whatwg.org/#the-title-element-2 */ +static const xmlNode *dom_get_title_element(const xmlDoc *doc) +{ + const xmlNode *node = doc->children; + + while (node != NULL) { + if (node->type == XML_ELEMENT_NODE) { + if (php_dom_ns_is_fast(node, php_dom_ns_is_html_magic_token) && xmlStrEqual(node->name, BAD_CAST "title")) { + break; + } + } + + node = php_dom_next_in_tree_order(node, NULL); + } + + return node; +} + +/* https://html.spec.whatwg.org/#document.title */ +zend_result dom_html_document_title_read(dom_object *obj, zval *retval) +{ + DOM_PROP_NODE(const xmlDoc *, docp, obj); + const xmlNode *root = xmlDocGetRootElement(docp); + + if (root == NULL) { + ZVAL_EMPTY_STRING(retval); + return SUCCESS; + } + + zend_string *value = zend_empty_string; + + /* 1. If the document element is an SVG svg element, + * then let value be the child text content of the first SVG title element that is a child of the document element. */ + if (php_dom_ns_is_fast(root, php_dom_ns_is_svg_magic_token) && xmlStrEqual(root->name, BAD_CAST "svg")) { + const xmlNode *cur = root->children; + + while (cur != NULL) { + if (cur->type == XML_ELEMENT_NODE + && php_dom_ns_is_fast(cur, php_dom_ns_is_svg_magic_token) && xmlStrEqual(cur->name, BAD_CAST "title")) { + value = dom_get_child_text_content(cur); + break; + } + cur = cur->next; + } + } else { + /* 2. Otherwise, let value be the child text content of the title element, + * or the empty string if the title element is null. */ + const xmlNode *title = dom_get_title_element(docp); + if (title != NULL) { + value = dom_get_child_text_content(title); + } + } + + /* 3. Strip and collapse ASCII whitespace in value. */ + value = dom_strip_and_collapse_ascii_whitespace(value); + + /* 4. Return value. */ + ZVAL_STR(retval, value); + + return SUCCESS; +} + #endif /* HAVE_LIBXML && HAVE_DOM */ diff --git a/ext/dom/infra.c b/ext/dom/infra.c new file mode 100644 index 0000000000000..8fa42453c7519 --- /dev/null +++ b/ext/dom/infra.c @@ -0,0 +1,77 @@ +/* + +----------------------------------------------------------------------+ + | 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 "config.h" +#endif + +#include "php.h" +#if defined(HAVE_LIBXML) && defined(HAVE_DOM) +#include "infra.h" + +/* https://infra.spec.whatwg.org/#ascii-whitespace */ +const char *ascii_whitespace = "\x09\x0A\x0C\x0D\x20"; + +/* https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace */ +zend_string *dom_strip_and_collapse_ascii_whitespace(zend_string *input) +{ + if (input == zend_empty_string) { + return input; + } + + ZEND_ASSERT(!ZSTR_IS_INTERNED(input)); + ZEND_ASSERT(GC_REFCOUNT(input) == 1); + + char *write_ptr = ZSTR_VAL(input); + + const char *start = ZSTR_VAL(input); + const char *current = start; + const char *end = current + ZSTR_LEN(input); + + current += strspn(current, ascii_whitespace); + + while (current < end) { + /* Copy non-whitespace */ + size_t non_whitespace_len = strcspn(current, ascii_whitespace); + /* If the pointers are equal, we still haven't encountered collapsable or strippable whitespace. */ + if (write_ptr != current) { + memmove(write_ptr, current, non_whitespace_len); + } + current += non_whitespace_len; + write_ptr += non_whitespace_len; + + /* Skip whitespace */ + current += strspn(current, ascii_whitespace); + if (current < end) { + /* Only make a space when we're not yet at the end of the input, because that means more non-whitespace + * input is to come. */ + *write_ptr++ = ' '; + } + } + + *write_ptr = '\0'; + + size_t len = write_ptr - start; + if (len != ZSTR_LEN(input)) { + return zend_string_truncate(input, len, false); + } else { + /* Forget the hash value since we may have transformed non-space-whitespace into spaces. */ + zend_string_forget_hash_val(input); + return input; + } +} + +#endif /* HAVE_LIBXML && HAVE_DOM */ diff --git a/ext/dom/infra.h b/ext/dom/infra.h new file mode 100644 index 0000000000000..d84ad5a2a0efa --- /dev/null +++ b/ext/dom/infra.h @@ -0,0 +1,26 @@ +/* + +----------------------------------------------------------------------+ + | 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 INFRA_H +#define INFRA_H + +#include "zend_string.h" + +extern const char *ascii_whitespace; + +zend_string *dom_strip_and_collapse_ascii_whitespace(zend_string *input); + +#endif diff --git a/ext/dom/php_dom.c b/ext/dom/php_dom.c index 3c26afd77800d..44fa0a5489d9d 100644 --- a/ext/dom/php_dom.c +++ b/ext/dom/php_dom.c @@ -853,6 +853,7 @@ PHP_MINIT_FUNCTION(dom) DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "childElementCount", dom_parent_node_child_element_count, NULL); DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "body", dom_html_document_body_read, dom_html_document_body_write); DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "head", dom_html_document_head_read, NULL); + DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "title", dom_html_document_title_read, NULL); zend_hash_merge(&dom_abstract_base_document_prop_handlers, &dom_modern_node_prop_handlers, NULL, false); /* No need to register in &classes because this is an abstract class handler. */ diff --git a/ext/dom/php_dom.stub.php b/ext/dom/php_dom.stub.php index a21b818b89994..e2d374b7efa4d 100644 --- a/ext/dom/php_dom.stub.php +++ b/ext/dom/php_dom.stub.php @@ -1584,6 +1584,7 @@ public function importLegacyNode(\DOMNode $node, bool $deep = false): Node {} public ?Element $body; /** @readonly */ public ?Element $head; + public string $title; } final class HTMLDocument extends Document diff --git a/ext/dom/php_dom_arginfo.h b/ext/dom/php_dom_arginfo.h index b364952687e7f..c3cc618757571 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: 0795d4e52f62ab33df92cfbdd5178223fbfc3eeb */ + * Stub hash: 7a2c28838f431eff28dea8cc5356dbcd38921592 */ 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) @@ -3456,6 +3456,12 @@ static zend_class_entry *register_class_Dom_Document(zend_class_entry *class_ent zend_declare_typed_property(class_entry, property_head_name, &property_head_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_head_class_Dom_Element, 0, MAY_BE_NULL)); zend_string_release(property_head_name); + zval property_title_default_value; + ZVAL_UNDEF(&property_title_default_value); + zend_string *property_title_name = zend_string_init("title", sizeof("title") - 1, 1); + zend_declare_typed_property(class_entry, property_title_name, &property_title_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + zend_string_release(property_title_name); + return class_entry; } diff --git a/ext/dom/tests/modern/common/Document_title_getter.phpt b/ext/dom/tests/modern/common/Document_title_getter.phpt new file mode 100644 index 0000000000000..eb4f4b7cdb5fd --- /dev/null +++ b/ext/dom/tests/modern/common/Document_title_getter.phpt @@ -0,0 +1,79 @@ +--TEST-- +Dom\Document::$title getter +--EXTENSIONS-- +dom +--FILE-- +A normal title without collapsable or strippable whitespace"); +var_dump($dom->title); + +$dom = Dom\XMLDocument::createFromString(" only ws at front"); +var_dump($dom->title); + +$dom = Dom\XMLDocument::createFromString("only ws at back "); +var_dump($dom->title); + +$dom = Dom\XMLDocument::createFromString("
first
second
"); +var_dump($dom->title); + +$dom = Dom\XMLDocument::createFromString("title"); +var_dump($dom->title); + +$dom = Dom\XMLDocument::createFromString(" abc def ghi "); +var_dump($dom->title); + +$dom = Dom\XMLDocument::createFromString(""); +var_dump($dom->title); + +$dom = Dom\XMLDocument::createFromString(""); +var_dump($dom->title); + +$dom = Dom\XMLDocument::createFromString(" \t\r\n "); +var_dump($dom->title); + +$dom = Dom\XMLDocument::createFromString(" \tx<?y y?><![CDATA[z]]>\n "); +var_dump($dom->title); + +$dom = Dom\XMLDocument::createFromString("<div><!-- comment -->x</div>y<p>z</p>w"); +var_dump($dom->title); + +$dom = Dom\XMLDocument::createFromString("title\nhere"); +var_dump($dom->title); + +echo "=== SVG namespaced root ===\n"; + +$dom = Dom\XMLDocument::createFromString("title"); +var_dump($dom->title); + +$dom = Dom\XMLDocument::createFromString("title"); +var_dump($dom->title); + +$dom = Dom\XMLDocument::createFromString("titlehi"); +var_dump($dom->title); + +$dom = Dom\XMLDocument::createFromString(""); +var_dump($dom->title); + +?> +--EXPECT-- +=== HTML namespaced root === +string(59) "A normal title without collapsable or strippable whitespace" +string(16) "only ws at front" +string(15) "only ws at back" +string(5) "first" +string(5) "title" +string(11) "abc def ghi" +string(0) "" +string(0) "" +string(0) "" +string(2) "xz" +string(2) "yw" +string(10) "title here" +=== SVG namespaced root === +string(5) "title" +string(5) "title" +string(5) "title" +string(0) "" diff --git a/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_01.phpt b/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_01.phpt index 593dae8e1f27e..cdd98b585268c 100644 --- a/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_01.phpt +++ b/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_01.phpt @@ -23,7 +23,7 @@ var_dump(get_class($dom->getElementsByTagName("p")->item(0))); ?> --EXPECT-- -object(Dom\HTMLDocument)#1 (27) { +object(Dom\HTMLDocument)#1 (28) { ["implementation"]=> string(22) "(object value omitted)" ["URL"]=> @@ -50,6 +50,8 @@ object(Dom\HTMLDocument)#1 (27) { string(22) "(object value omitted)" ["head"]=> string(22) "(object value omitted)" + ["title"]=> + string(0) "" ["nodeType"]=> int(13) ["nodeName"]=> diff --git a/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_02.phpt b/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_02.phpt index b26205647786d..b216b399a16d4 100644 --- a/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_02.phpt +++ b/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_02.phpt @@ -23,7 +23,7 @@ var_dump(get_class($dom->getElementsByTagName("p")->item(0))); ?> --EXPECT-- -object(Dom\HTMLDocument)#1 (27) { +object(Dom\HTMLDocument)#1 (28) { ["implementation"]=> string(22) "(object value omitted)" ["URL"]=> @@ -50,6 +50,8 @@ object(Dom\HTMLDocument)#1 (27) { string(22) "(object value omitted)" ["head"]=> string(22) "(object value omitted)" + ["title"]=> + string(0) "" ["nodeType"]=> int(13) ["nodeName"]=> diff --git a/ext/dom/tests/modern/spec/Document_implementation_createDocument.phpt b/ext/dom/tests/modern/spec/Document_implementation_createDocument.phpt index 37d73c5d40a7f..ae49a6a494c9a 100644 --- a/ext/dom/tests/modern/spec/Document_implementation_createDocument.phpt +++ b/ext/dom/tests/modern/spec/Document_implementation_createDocument.phpt @@ -37,7 +37,7 @@ echo $dom->implementation->createDocument(null, "", $dtd)->saveXml(), "\n"; ?> --EXPECT-- --- (null, "") --- -object(Dom\XMLDocument)#3 (31) { +object(Dom\XMLDocument)#3 (32) { ["xmlEncoding"]=> string(5) "UTF-8" ["xmlStandalone"]=> @@ -72,6 +72,8 @@ object(Dom\XMLDocument)#3 (31) { NULL ["head"]=> NULL + ["title"]=> + string(0) "" ["nodeType"]=> int(9) ["nodeName"]=> diff --git a/ext/dom/tests/modern/xml/XMLDocument_debug.phpt b/ext/dom/tests/modern/xml/XMLDocument_debug.phpt index a0f6a528389b6..e2d6ebffe89cd 100644 --- a/ext/dom/tests/modern/xml/XMLDocument_debug.phpt +++ b/ext/dom/tests/modern/xml/XMLDocument_debug.phpt @@ -10,7 +10,7 @@ var_dump($dom); ?> --EXPECT-- -object(Dom\XMLDocument)#1 (31) { +object(Dom\XMLDocument)#1 (32) { ["xmlEncoding"]=> string(5) "UTF-8" ["xmlStandalone"]=> @@ -45,6 +45,8 @@ object(Dom\XMLDocument)#1 (31) { NULL ["head"]=> NULL + ["title"]=> + string(0) "" ["nodeType"]=> int(9) ["nodeName"]=> diff --git a/ext/dom/tests/modern/xml/XMLDocument_fromEmptyDocument_02.phpt b/ext/dom/tests/modern/xml/XMLDocument_fromEmptyDocument_02.phpt index b8a87e5ba4ea6..62d64a05f9b2a 100644 --- a/ext/dom/tests/modern/xml/XMLDocument_fromEmptyDocument_02.phpt +++ b/ext/dom/tests/modern/xml/XMLDocument_fromEmptyDocument_02.phpt @@ -10,7 +10,7 @@ var_dump($dom); ?> --EXPECT-- -object(Dom\XMLDocument)#1 (31) { +object(Dom\XMLDocument)#1 (32) { ["xmlEncoding"]=> string(5) "UTF-8" ["xmlStandalone"]=> @@ -45,6 +45,8 @@ object(Dom\XMLDocument)#1 (31) { NULL ["head"]=> NULL + ["title"]=> + string(0) "" ["nodeType"]=> int(9) ["nodeName"]=> diff --git a/ext/dom/tests/modern/xml/XMLDocument_node_ownerDocument_for_XML.phpt b/ext/dom/tests/modern/xml/XMLDocument_node_ownerDocument_for_XML.phpt index 4f3308343e994..e18c43f05ae82 100644 --- a/ext/dom/tests/modern/xml/XMLDocument_node_ownerDocument_for_XML.phpt +++ b/ext/dom/tests/modern/xml/XMLDocument_node_ownerDocument_for_XML.phpt @@ -13,7 +13,7 @@ var_dump($element->ownerDocument); ?> --EXPECTF-- -object(Dom\XMLDocument)#1 (31) { +object(Dom\XMLDocument)#1 (32) { ["xmlEncoding"]=> string(5) "UTF-8" ["xmlStandalone"]=> @@ -48,6 +48,8 @@ object(Dom\XMLDocument)#1 (31) { NULL ["head"]=> NULL + ["title"]=> + string(0) "" ["nodeType"]=> int(9) ["nodeName"]=> From 0d3042d5b5b539eefaa6f317d46480e5cb137167 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sun, 7 Apr 2024 17:06:29 +0200 Subject: [PATCH 5/7] Implement Dom\Document::$title setter --- ext/dom/dom_properties.h | 1 + ext/dom/html_document.c | 130 ++++++++++++++++-- ext/dom/php_dom.c | 2 +- .../modern/common/Document_title_setter.phpt | 105 ++++++++++++++ 4 files changed, 223 insertions(+), 15 deletions(-) create mode 100644 ext/dom/tests/modern/common/Document_title_setter.phpt diff --git a/ext/dom/dom_properties.h b/ext/dom/dom_properties.h index 039f7fd260e1c..a9b6df87594fe 100644 --- a/ext/dom/dom_properties.h +++ b/ext/dom/dom_properties.h @@ -66,6 +66,7 @@ zend_result dom_html_document_body_read(dom_object *obj, zval *retval); zend_result dom_html_document_body_write(dom_object *obj, zval *newval); zend_result dom_html_document_head_read(dom_object *obj, zval *retval); zend_result dom_html_document_title_read(dom_object *obj, zval *retval); +zend_result dom_html_document_title_write(dom_object *obj, zval *newval); /* documenttype properties */ zend_result dom_documenttype_name_read(dom_object *obj, zval *retval); diff --git a/ext/dom/html_document.c b/ext/dom/html_document.c index d487d04b7fbbe..bf1186074e8a4 100644 --- a/ext/dom/html_document.c +++ b/ext/dom/html_document.c @@ -1359,14 +1359,14 @@ zend_result dom_html_document_encoding_write(dom_object *obj, zval *newval) return SUCCESS; } -static const xmlNode *dom_html_document_element_read_raw(const xmlDoc *docp, bool (*accept)(const xmlChar *)) +static xmlNodePtr dom_html_document_element_read_raw(const xmlDoc *docp, bool (*accept)(const xmlChar *)) { const xmlNode *root = xmlDocGetRootElement(docp); if (root == NULL || !(php_dom_ns_is_fast(root, php_dom_ns_is_html_magic_token) && xmlStrEqual(root->name, BAD_CAST "html"))) { return NULL; } - const xmlNode *cur = root->children; + xmlNodePtr cur = root->children; while (cur != NULL) { if (cur->type == XML_ELEMENT_NODE && php_dom_ns_is_fast(cur, php_dom_ns_is_html_magic_token) && accept(cur->name)) { return cur; @@ -1476,9 +1476,9 @@ static zend_string *dom_get_child_text_content(const xmlNode *node) } /* https://html.spec.whatwg.org/#the-title-element-2 */ -static const xmlNode *dom_get_title_element(const xmlDoc *doc) +static xmlNodePtr dom_get_title_element(const xmlDoc *doc) { - const xmlNode *node = doc->children; + xmlNodePtr node = doc->children; while (node != NULL) { if (node->type == XML_ELEMENT_NODE) { @@ -1493,11 +1493,28 @@ static const xmlNode *dom_get_title_element(const xmlDoc *doc) return node; } +/* The subtle difference is that this is about the direct title descendant of the svg element, + * whereas the html variant of this function is about the first in-tree title element. */ +static xmlNodePtr dom_get_svg_title_element(xmlNodePtr svg) +{ + xmlNodePtr cur = svg->children; + + while (cur != NULL) { + if (cur->type == XML_ELEMENT_NODE + && php_dom_ns_is_fast(cur, php_dom_ns_is_svg_magic_token) && xmlStrEqual(cur->name, BAD_CAST "title")) { + break; + } + cur = cur->next; + } + + return cur; +} + /* https://html.spec.whatwg.org/#document.title */ zend_result dom_html_document_title_read(dom_object *obj, zval *retval) { DOM_PROP_NODE(const xmlDoc *, docp, obj); - const xmlNode *root = xmlDocGetRootElement(docp); + xmlNodePtr root = xmlDocGetRootElement(docp); if (root == NULL) { ZVAL_EMPTY_STRING(retval); @@ -1509,15 +1526,9 @@ zend_result dom_html_document_title_read(dom_object *obj, zval *retval) /* 1. If the document element is an SVG svg element, * then let value be the child text content of the first SVG title element that is a child of the document element. */ if (php_dom_ns_is_fast(root, php_dom_ns_is_svg_magic_token) && xmlStrEqual(root->name, BAD_CAST "svg")) { - const xmlNode *cur = root->children; - - while (cur != NULL) { - if (cur->type == XML_ELEMENT_NODE - && php_dom_ns_is_fast(cur, php_dom_ns_is_svg_magic_token) && xmlStrEqual(cur->name, BAD_CAST "title")) { - value = dom_get_child_text_content(cur); - break; - } - cur = cur->next; + const xmlNode *title = dom_get_svg_title_element(root); + if (title != NULL) { + value = dom_get_child_text_content(title); } } else { /* 2. Otherwise, let value be the child text content of the title element, @@ -1537,4 +1548,95 @@ zend_result dom_html_document_title_read(dom_object *obj, zval *retval) return SUCCESS; } +static void dom_string_replace_all(xmlDocPtr docp, xmlNodePtr element, zval *zv) +{ + dom_remove_all_children(element); + xmlNode *text = xmlNewDocText(docp, BAD_CAST Z_STRVAL_P(zv)); + xmlAddChild(element, text); +} + +/* https://html.spec.whatwg.org/#document.title */ +zend_result dom_html_document_title_write(dom_object *obj, zval *newval) +{ + DOM_PROP_NODE(xmlDocPtr, docp, obj); + xmlNodePtr root = xmlDocGetRootElement(docp); + + if (root == NULL) { + return SUCCESS; + } + + /* If the document element is an SVG svg element */ + if (php_dom_ns_is_fast(root, php_dom_ns_is_svg_magic_token) && xmlStrEqual(root->name, BAD_CAST "svg")) { + /* 1. If there is an SVG title element that is a child of the document element, let element be the first such element. */ + xmlNodePtr element = dom_get_svg_title_element(root); + + /* 2. Otherwise: */ + if (element == NULL) { + /* 2.1. Let element be the result of creating an element given the document element's node document, + * title, and the SVG namespace. */ + + /* Annoyingly, we must create it in the svg namespace _without_ prefix... */ + xmlNsPtr ns = root->ns; + if (ns->prefix != NULL) { + /* Slow path... */ + php_dom_libxml_ns_mapper *ns_mapper = php_dom_get_ns_mapper(obj); + zend_string *href = ZSTR_INIT_LITERAL(DOM_SVG_NS_URI, false); + ns = php_dom_libxml_ns_mapper_get_ns(ns_mapper, zend_empty_string, href); + zend_string_release_ex(href, false); + } + + element = xmlNewDocNode(docp, ns, BAD_CAST "title", NULL); + if (UNEXPECTED(element == NULL)) { + php_dom_throw_error(INVALID_STATE_ERR, true); + return FAILURE; + } + + /* 2.2. Insert element as the first child of the document element. */ + if (root->children == NULL) { + root->last = element; + } else { + element->next = root->children; + root->children->prev = element; + } + root->children = element; + element->parent = root; + } + + /* 3. String replace all with the given value within element. */ + dom_string_replace_all(docp, element, newval); + } + /* If the document element is in the HTML namespace */ + else if (php_dom_ns_is_fast(root, php_dom_ns_is_html_magic_token)) { + /* 1. If the title element is null and the head element is null, then return. */ + xmlNodePtr title = dom_get_title_element(docp); + xmlNodePtr head = dom_html_document_element_read_raw(docp, dom_accept_head_name); + if (title == NULL && head == NULL) { + return SUCCESS; + } + + /* 2. If the title element is non-null, let element be the title element. */ + xmlNodePtr element = title; + + /* 3. Otherwise: */ + if (element == NULL) { + /* 3.1. Let element be the result of creating an element given the document element's node document, title, + * and the HTML namespace. */ + php_dom_libxml_ns_mapper *ns_mapper = php_dom_get_ns_mapper(obj); + element = xmlNewDocNode(docp, php_dom_libxml_ns_mapper_ensure_html_ns(ns_mapper), BAD_CAST "title", NULL); + if (UNEXPECTED(element == NULL)) { + php_dom_throw_error(INVALID_STATE_ERR, true); + return FAILURE; + } + + /* 3.2. Append element to the head element. */ + xmlAddChild(head, element); + } + + /* 4. String replace all with the given value within element. */ + dom_string_replace_all(docp, element, newval); + } + + return SUCCESS; +} + #endif /* HAVE_LIBXML && HAVE_DOM */ diff --git a/ext/dom/php_dom.c b/ext/dom/php_dom.c index 44fa0a5489d9d..4c239ee896b5a 100644 --- a/ext/dom/php_dom.c +++ b/ext/dom/php_dom.c @@ -853,7 +853,7 @@ PHP_MINIT_FUNCTION(dom) DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "childElementCount", dom_parent_node_child_element_count, NULL); DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "body", dom_html_document_body_read, dom_html_document_body_write); DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "head", dom_html_document_head_read, NULL); - DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "title", dom_html_document_title_read, NULL); + DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "title", dom_html_document_title_read, dom_html_document_title_write); zend_hash_merge(&dom_abstract_base_document_prop_handlers, &dom_modern_node_prop_handlers, NULL, false); /* No need to register in &classes because this is an abstract class handler. */ diff --git a/ext/dom/tests/modern/common/Document_title_setter.phpt b/ext/dom/tests/modern/common/Document_title_setter.phpt new file mode 100644 index 0000000000000..9cf91b0339b81 --- /dev/null +++ b/ext/dom/tests/modern/common/Document_title_setter.phpt @@ -0,0 +1,105 @@ +--TEST-- +Dom\Document::$title setter +--EXTENSIONS-- +dom +--FILE-- +'); +$dom->title = "hello & world"; +echo $dom->saveXML(), "\n"; + +$dom = Dom\XMLDocument::createFromString(''); +$dom->title = "hello & world"; +echo $dom->saveXML(), "\n"; +$dom->title = ""; +echo $dom->saveXML(), "\n"; +$dom->title = "test"; +echo $dom->saveXML(), "\n"; + +$dom = Dom\XMLDocument::createFromString(''); +$dom->title = "test"; +echo $dom->saveXML(), "\n"; +var_dump($dom->documentElement->firstElementChild->prefix, $dom->documentElement->firstElementChild->namespaceURI); + +$dom = Dom\XMLDocument::createFromString('first node
'); +$dom->title = "test"; +echo $dom->saveXML(), "\n"; +$dom->documentElement->firstElementChild->remove(); +$dom->title = "test2"; +echo $dom->saveXML(), "\n"; + +echo "\n=== HTML namespaced test ===\n\n"; + +$dom = Dom\XMLDocument::createFromString(''); +$dom->title = "test"; +echo $dom->saveXML(), "\n"; + +$dom = Dom\XMLDocument::createFromString(''); +$dom->title = "test"; +echo $dom->saveXML(), "\n"; + +$dom = Dom\XMLDocument::createFromString(''); +$dom->title = "test"; +echo $dom->saveXML(), "\n"; + +$dom = Dom\XMLDocument::createFromString(''); +$dom->title = "test"; +echo $dom->saveXML(), "\n"; + +$dom = Dom\XMLDocument::createFromString('foo<div/>'); +$dom->title = "test"; +echo $dom->saveXML(), "\n"; + +echo "\n=== neither namespaced test ===\n\n"; + +$dom = Dom\XMLDocument::createEmpty(); +$dom->title = ""; +echo $dom->saveXML(), "\n"; + +$dom = Dom\XMLDocument::createFromString(''); +$dom->title = "test"; +echo $dom->saveXML(), "\n"; + +?> +--EXPECT-- +=== SVG namespaced test === + + + + +hello &amp; world + + + +test + +test +NULL +string(26) "http://www.w3.org/2000/svg" + +testfirst node
+ +test2first node
+ +=== HTML namespaced test === + + + + + + +test + +test + +test + +=== neither namespaced test === + + + + + From 231e5e34d705e3bf1c6b543b9eb9c24f1c6f42ef Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sat, 27 Apr 2024 21:23:29 +0200 Subject: [PATCH 6/7] Implement Dom\HTMLElement class --- ext/dom/dom_ce.h | 1 + ext/dom/html_document.c | 3 ++- ext/dom/internal_helpers.h | 1 - ext/dom/php_dom.c | 21 +++++++++++++++- ext/dom/php_dom.stub.php | 8 +++++-- ext/dom/php_dom_arginfo.h | 24 +++++++++++++++---- .../Document_body_setter_errors.phpt | 6 ++--- .../HTMLDocument_registerNodeClass_03.phpt | 4 ++-- ...should_retain_properties_and_owner_01.phpt | 4 ++-- ...should_retain_properties_and_owner_02.phpt | 4 ++-- .../modern/spec/Element_prefix_readonly.phpt | 2 +- 11 files changed, 58 insertions(+), 20 deletions(-) diff --git a/ext/dom/dom_ce.h b/ext/dom/dom_ce.h index 4dee34305331f..13e58957df313 100644 --- a/ext/dom/dom_ce.h +++ b/ext/dom/dom_ce.h @@ -44,6 +44,7 @@ extern PHP_DOM_EXPORT zend_class_entry *dom_attr_class_entry; extern PHP_DOM_EXPORT zend_class_entry *dom_modern_attr_class_entry; extern PHP_DOM_EXPORT zend_class_entry *dom_element_class_entry; extern PHP_DOM_EXPORT zend_class_entry *dom_modern_element_class_entry; +extern PHP_DOM_EXPORT zend_class_entry *dom_html_element_class_entry; extern PHP_DOM_EXPORT zend_class_entry *dom_text_class_entry; extern PHP_DOM_EXPORT zend_class_entry *dom_modern_text_class_entry; extern PHP_DOM_EXPORT zend_class_entry *dom_comment_class_entry; diff --git a/ext/dom/html_document.c b/ext/dom/html_document.c index bf1186074e8a4..7d3670208887b 100644 --- a/ext/dom/html_document.c +++ b/ext/dom/html_document.c @@ -1423,7 +1423,8 @@ zend_result dom_html_document_body_write(dom_object *obj, zval *newval) dom_object *newval_intern = Z_DOMOBJ_P(newval); if (newval_intern->ptr != NULL) { xmlNodePtr newval_node = ((php_libxml_node_ptr *) newval_intern->ptr)->node; - if (php_dom_ns_is_fast(newval_node, php_dom_ns_is_html_magic_token) && dom_accept_body_name(newval_node->name)) { + /* Note: because this property has type HTMLElement, we know the namespace is correct. */ + if (dom_accept_body_name(newval_node->name)) { /* 2. If the new value is the same as the body element, return. */ const xmlNode *current_body_element = dom_html_document_element_read_raw(docp, dom_accept_body_name); if (current_body_element == newval_node) { diff --git a/ext/dom/internal_helpers.h b/ext/dom/internal_helpers.h index a19367dd47770..b74a735ad2eeb 100644 --- a/ext/dom/internal_helpers.h +++ b/ext/dom/internal_helpers.h @@ -47,7 +47,6 @@ static zend_always_inline zend_class_entry *dom_get_dtd_namednodemap_ce(bool mod DOM_DEF_GET_CE_FUNC(node) DOM_DEF_GET_CE_FUNC(documenttype) -DOM_DEF_GET_CE_FUNC(element) DOM_DEF_GET_CE_FUNC(attr) DOM_DEF_GET_CE_FUNC(entity) DOM_DEF_GET_CE_FUNC(entityreference) diff --git a/ext/dom/php_dom.c b/ext/dom/php_dom.c index 4c239ee896b5a..83dc12c6428d6 100644 --- a/ext/dom/php_dom.c +++ b/ext/dom/php_dom.c @@ -63,6 +63,7 @@ PHP_DOM_EXPORT zend_class_entry *dom_attr_class_entry; PHP_DOM_EXPORT zend_class_entry *dom_modern_attr_class_entry; PHP_DOM_EXPORT zend_class_entry *dom_element_class_entry; PHP_DOM_EXPORT zend_class_entry *dom_modern_element_class_entry; +PHP_DOM_EXPORT zend_class_entry *dom_html_element_class_entry; PHP_DOM_EXPORT zend_class_entry *dom_text_class_entry; PHP_DOM_EXPORT zend_class_entry *dom_modern_text_class_entry; PHP_DOM_EXPORT zend_class_entry *dom_comment_class_entry; @@ -1042,6 +1043,11 @@ PHP_MINIT_FUNCTION(dom) DOM_OVERWRITE_PROP_HANDLER(&dom_modern_element_prop_handlers, "textContent", dom_node_text_content_read, dom_node_text_content_write); zend_hash_add_new_ptr(&classes, dom_modern_element_class_entry->name, &dom_modern_element_prop_handlers); + dom_html_element_class_entry = register_class_Dom_HTMLElement(dom_modern_element_class_entry); + dom_html_element_class_entry->create_object = dom_objects_new; + dom_html_element_class_entry->default_object_handlers = &dom_object_handlers; + zend_hash_add_new_ptr(&classes, dom_html_element_class_entry->name, &dom_modern_element_prop_handlers); + dom_text_class_entry = register_class_DOMText(dom_characterdata_class_entry); dom_text_class_entry->create_object = dom_objects_new; dom_text_class_entry->default_object_handlers = &dom_object_handlers; @@ -1541,6 +1547,19 @@ void php_dom_create_iterator(zval *return_value, dom_iterator_type iterator_type } /* }}} */ +static zend_always_inline zend_class_entry *dom_get_element_ce(const xmlNode *node, bool modern) +{ + if (modern) { + if (php_dom_ns_is_fast(node, php_dom_ns_is_html_magic_token)) { + return dom_html_element_class_entry; + } else { + return dom_modern_element_class_entry; + } + } else { + return dom_element_class_entry; + } +} + /* {{{ php_dom_create_object */ PHP_DOM_EXPORT bool php_dom_create_object(xmlNodePtr obj, zval *return_value, dom_object *domobj) { @@ -1572,7 +1591,7 @@ PHP_DOM_EXPORT bool php_dom_create_object(xmlNodePtr obj, zval *return_value, do } case XML_ELEMENT_NODE: { - ce = dom_get_element_ce(modern); + ce = dom_get_element_ce(obj, modern); break; } case XML_ATTRIBUTE_NODE: diff --git a/ext/dom/php_dom.stub.php b/ext/dom/php_dom.stub.php index e2d374b7efa4d..1f6f2bb8bc876 100644 --- a/ext/dom/php_dom.stub.php +++ b/ext/dom/php_dom.stub.php @@ -1374,6 +1374,10 @@ public function prepend(Node|string ...$nodes): void {} public function replaceChildren(Node|string ...$nodes): void {} } + class HTMLElement extends Element + { + } + class Attr extends Node { /** @readonly */ @@ -1581,9 +1585,9 @@ public function replaceChildren(Node|string ...$nodes): void {} public function importLegacyNode(\DOMNode $node, bool $deep = false): Node {} - public ?Element $body; + public ?HTMLElement $body; /** @readonly */ - public ?Element $head; + public ?HTMLElement $head; public string $title; } diff --git a/ext/dom/php_dom_arginfo.h b/ext/dom/php_dom_arginfo.h index c3cc618757571..dae80f6fe3cfd 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: 7a2c28838f431eff28dea8cc5356dbcd38921592 */ + * Stub hash: eda699f0d524fae5ae76a3a395438a16989c2af8 */ 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) @@ -1674,6 +1674,10 @@ static const zend_function_entry class_Dom_Element_methods[] = { ZEND_FE_END }; +static const zend_function_entry class_Dom_HTMLElement_methods[] = { + ZEND_FE_END +}; + static const zend_function_entry class_Dom_Attr_methods[] = { ZEND_RAW_FENTRY("isId", zim_DOMAttr_isId, arginfo_class_Dom_Attr_isId, ZEND_ACC_PUBLIC, NULL, NULL) ZEND_FE_END @@ -3080,6 +3084,16 @@ static zend_class_entry *register_class_Dom_Element(zend_class_entry *class_entr return class_entry; } +static zend_class_entry *register_class_Dom_HTMLElement(zend_class_entry *class_entry_Dom_Element) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Dom", "HTMLElement", class_Dom_HTMLElement_methods); + class_entry = zend_register_internal_class_ex(&ce, class_entry_Dom_Element); + + return class_entry; +} + static zend_class_entry *register_class_Dom_Attr(zend_class_entry *class_entry_Dom_Node) { zend_class_entry ce, *class_entry; @@ -3445,15 +3459,15 @@ static zend_class_entry *register_class_Dom_Document(zend_class_entry *class_ent zval property_body_default_value; ZVAL_UNDEF(&property_body_default_value); zend_string *property_body_name = zend_string_init("body", sizeof("body") - 1, 1); - zend_string *property_body_class_Dom_Element = zend_string_init("Dom\\Element", sizeof("Dom\\Element")-1, 1); - zend_declare_typed_property(class_entry, property_body_name, &property_body_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_body_class_Dom_Element, 0, MAY_BE_NULL)); + zend_string *property_body_class_Dom_HTMLElement = zend_string_init("Dom\\HTMLElement", sizeof("Dom\\HTMLElement")-1, 1); + zend_declare_typed_property(class_entry, property_body_name, &property_body_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_body_class_Dom_HTMLElement, 0, MAY_BE_NULL)); zend_string_release(property_body_name); zval property_head_default_value; ZVAL_UNDEF(&property_head_default_value); zend_string *property_head_name = zend_string_init("head", sizeof("head") - 1, 1); - zend_string *property_head_class_Dom_Element = zend_string_init("Dom\\Element", sizeof("Dom\\Element")-1, 1); - zend_declare_typed_property(class_entry, property_head_name, &property_head_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_head_class_Dom_Element, 0, MAY_BE_NULL)); + zend_string *property_head_class_Dom_HTMLElement = zend_string_init("Dom\\HTMLElement", sizeof("Dom\\HTMLElement")-1, 1); + zend_declare_typed_property(class_entry, property_head_name, &property_head_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_head_class_Dom_HTMLElement, 0, MAY_BE_NULL)); zend_string_release(property_head_name); zval property_title_default_value; diff --git a/ext/dom/tests/modern/html/interactions/Document_body_setter_errors.phpt b/ext/dom/tests/modern/html/interactions/Document_body_setter_errors.phpt index 9913dbb12ae91..1f934fcff890d 100644 --- a/ext/dom/tests/modern/html/interactions/Document_body_setter_errors.phpt +++ b/ext/dom/tests/modern/html/interactions/Document_body_setter_errors.phpt @@ -11,7 +11,7 @@ function testNormalReplace($cb) var_dump($dom->body?->nodeName); try { $dom->body = $cb($dom); - } catch (DOMException $e) { + } catch (Throwable $e) { echo $e->getMessage(), "\n"; } var_dump($dom->body?->nodeName); @@ -50,11 +50,11 @@ The new body must either be a body or a frameset tag string(4) "BODY" --- Right element tag in wrong namespace --- string(4) "BODY" -The new body must either be a body or a frameset tag +Cannot assign Dom\Element to property Dom\Document::$body of type ?Dom\HTMLElement string(4) "BODY" --- Right element tag in no namespace --- string(4) "BODY" -The new body must either be a body or a frameset tag +Cannot assign Dom\Element to property Dom\Document::$body of type ?Dom\HTMLElement string(4) "BODY" --- Set body without document element --- A body can only be set if there is a document element diff --git a/ext/dom/tests/modern/html/interactions/HTMLDocument_registerNodeClass_03.phpt b/ext/dom/tests/modern/html/interactions/HTMLDocument_registerNodeClass_03.phpt index 7a4d2d32797dc..6616eb5d11465 100644 --- a/ext/dom/tests/modern/html/interactions/HTMLDocument_registerNodeClass_03.phpt +++ b/ext/dom/tests/modern/html/interactions/HTMLDocument_registerNodeClass_03.phpt @@ -5,7 +5,7 @@ dom --FILE-- foo
", LIBXML_NOERROR); -$dom->registerNodeClass("Dom\\Element", "Custom"); +$dom->registerNodeClass("Dom\\HTMLElement", "Custom"); var_dump($dom->getElementsByTagName('div')[0]->reverseTagName()); diff --git a/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_01.phpt b/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_01.phpt index cdd98b585268c..5051c3f9aabf6 100644 --- a/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_01.phpt +++ b/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_01.phpt @@ -5,10 +5,10 @@ dom --FILE-- foo

", LIBXML_NOERROR); -$dom->registerNodeClass("Dom\\Element", "MyElement"); +$dom->registerNodeClass("Dom\\HTMLElement", "MyElement"); // Destroy reference to the DOM $child = $dom->documentElement; diff --git a/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_02.phpt b/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_02.phpt index b216b399a16d4..b160c72f0a54f 100644 --- a/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_02.phpt +++ b/ext/dom/tests/modern/html/interactions/HTMLDocument_should_retain_properties_and_owner_02.phpt @@ -5,10 +5,10 @@ dom --FILE-- foo

", LIBXML_NOERROR); -$dom->registerNodeClass("Dom\\Element", "MyElement"); +$dom->registerNodeClass("Dom\\HTMLElement", "MyElement"); $child = $dom->documentElement->appendChild($dom->createElement('html')); // Destroy reference to the DOM diff --git a/ext/dom/tests/modern/spec/Element_prefix_readonly.phpt b/ext/dom/tests/modern/spec/Element_prefix_readonly.phpt index 7ebc3efaadc2b..78625fcb6f7d3 100644 --- a/ext/dom/tests/modern/spec/Element_prefix_readonly.phpt +++ b/ext/dom/tests/modern/spec/Element_prefix_readonly.phpt @@ -14,5 +14,5 @@ try { echo $dom->saveXml(); ?> --EXPECT-- -Cannot modify readonly property Dom\Element::$prefix +Cannot modify readonly property Dom\HTMLElement::$prefix From 190ef42741a6a11e8c3640005f350edc399106af Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Wed, 26 Jun 2024 20:39:05 +0200 Subject: [PATCH 7/7] NEWS and UPGRADING for new DOM features RFC --- NEWS | 1 + UPGRADING | 2 ++ 2 files changed, 3 insertions(+) diff --git a/NEWS b/NEWS index 1e327fab6e04a..5191a9fd554cd 100644 --- a/NEWS +++ b/NEWS @@ -55,6 +55,7 @@ PHP NEWS . Implemented opt-in ext/dom spec compliance RFC. (nielsdos) . Fixed bug #79701 (getElementById does not correctly work with duplicate definitions). (nielsdos) + . Implemented "New ext-dom features in PHP 8.4" RFC. (nielsdos) - Fileinfo: . Update to libmagic 5.45. (nielsdos) diff --git a/UPGRADING b/UPGRADING index 72eed3e71ac19..07cc55ce12dd8 100644 --- a/UPGRADING +++ b/UPGRADING @@ -639,6 +639,8 @@ PHP 8.4 UPGRADE NOTES equivalents to the old DOM classes in the global namespaces. The new classes follow the DOM living spec. RFC: https://wiki.php.net/rfc/opt_in_dom_spec_compliance + . Implemented "New ext-dom features in PHP 8.4" RFC. + RFC: https://wiki.php.net/rfc/dom_additions_84 ======================================== 8. Removed Extensions and SAPIs