<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[58992] trunk: HTML API: Only examine HTML nodes in `pop_until()` instack of open elements.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/58992">58992</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/58992","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>dmsnell</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2024-09-04 19:23:48 +0000 (Wed, 04 Sep 2024)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>HTML API: Only examine HTML nodes in `pop_until()` instack of open elements.

The `pop_until( $tag_name )` method in the stack of open elements should only be examining HTML elements, but it has only been checking the tag name. This has led to closing the wrong tags when run from inside foreign content. A very specific situation where this may arise is when a `TEMPLATE` closer is found inside foreign content, inside another template.

{{{
HTML:template   SVG:template                 HTML:/template
<template><svg><template><foreignObject><div></template><div>
{U+2570}{U+2500}{U+2500}< this outer TEMPLATE is closed by this one >{U+2500}{U+2500}{U+2500}{U+256F}
}}}

This patch constains the method to checking for elements matching the tag name which are in the HTML namespace so that the proper detection occurs.

Developed in https://github.com/WordPress/wordpress-develop/pull/7286
Discussed in https://core.trac.wordpress.org/ticket/61576

Follow-up to <a href="https://core.trac.wordpress.org/changeset/58867">[58867]</a>.

Props dmsnell, jonsurrell.
See <a href="https://core.trac.wordpress.org/ticket/61576">#61576</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludeshtmlapiclasswphtmlopenelementsphp">trunk/src/wp-includes/html-api/class-wp-html-open-elements.php</a></li>
<li><a href="#trunksrcwpincludeshtmlapiclasswphtmlprocessorphp">trunk/src/wp-includes/html-api/class-wp-html-processor.php</a></li>
<li><a href="#trunktestsphpunittestshtmlapiwpHtmlProcessorphp">trunk/tests/phpunit/tests/html-api/wpHtmlProcessor.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludeshtmlapiclasswphtmlopenelementsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/html-api/class-wp-html-open-elements.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/html-api/class-wp-html-open-elements.php    2024-09-04 18:32:06 UTC (rev 58991)
+++ trunk/src/wp-includes/html-api/class-wp-html-open-elements.php      2024-09-04 19:23:48 UTC (rev 58992)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -530,31 +530,31 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * Pops nodes off of the stack of open elements until one with the given tag name has been popped.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Pops nodes off of the stack of open elements until an HTML tag with the given name has been popped.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 6.4.0
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @see WP_HTML_Open_Elements::pop
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param string $tag_name Name of tag that needs to be popped off of the stack of open elements.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param string $html_tag_name Name of tag that needs to be popped off of the stack of open elements.
</ins><span class="cx" style="display: block; padding: 0 10px">          * @return bool Whether a tag of the given name was found and popped off of the stack of open elements.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        public function pop_until( string $tag_name ): bool {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function pop_until( string $html_tag_name ): bool {
</ins><span class="cx" style="display: block; padding: 0 10px">                 foreach ( $this->walk_up() as $item ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( 'context-node' === $item->bookmark_name ) {
-                               return true;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $this->pop();
+
+                       if ( 'html' !== $item->namespace ) {
+                               continue;
</ins><span class="cx" style="display: block; padding: 0 10px">                         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        $this->pop();
-
</del><span class="cx" style="display: block; padding: 0 10px">                         if (
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                '(internal: H1 through H6 - do not use)' === $tag_name &&
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         '(internal: H1 through H6 - do not use)' === $html_tag_name &&
</ins><span class="cx" style="display: block; padding: 0 10px">                                 in_array( $item->node_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true )
</span><span class="cx" style="display: block; padding: 0 10px">                        ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                return true;
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( $tag_name === $item->node_name ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( $html_tag_name === $item->node_name ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                 return true;
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span></span></pre></div>
<a id="trunksrcwpincludeshtmlapiclasswphtmlprocessorphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/html-api/class-wp-html-processor.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/html-api/class-wp-html-processor.php        2024-09-04 18:32:06 UTC (rev 58991)
+++ trunk/src/wp-includes/html-api/class-wp-html-processor.php  2024-09-04 19:23:48 UTC (rev 58992)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5428,6 +5428,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        // All of the following rules are for matching HTML elements.
+                       if ( 'html' !== $node->namespace ) {
+                               continue;
+                       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         switch ( $node->node_name ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                /*
</span><span class="cx" style="display: block; padding: 0 10px">                                 * > 4. If node is a `select` element, run these substeps:
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5443,6 +5448,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                case 'SELECT':
</span><span class="cx" style="display: block; padding: 0 10px">                                        if ( ! $last ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                                foreach ( $this->state->stack_of_open_elements->walk_up( $node ) as $ancestor ) {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                                        if ( 'html' !== $ancestor->namespace ) {
+                                                               continue;
+                                                       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                                                         switch ( $ancestor->node_name ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                                                /*
</span><span class="cx" style="display: block; padding: 0 10px">                                                                 * > 5. If _ancestor_ is a `template` node, jump to the step below
</span></span></pre></div>
<a id="trunktestsphpunittestshtmlapiwpHtmlProcessorphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/html-api/wpHtmlProcessor.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/html-api/wpHtmlProcessor.php    2024-09-04 18:32:06 UTC (rev 58991)
+++ trunk/tests/phpunit/tests/html-api/wpHtmlProcessor.php      2024-09-04 19:23:48 UTC (rev 58992)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -521,6 +521,30 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Ensures that the HTML Processor correctly handles TEMPLATE tag closing and namespaces.
+        *
+        * This is a tricky test case that corresponds to the html5lib tests "template/line1466".
+        *
+        * When the `</template>` token is reached it is in the HTML namespace (thanks to the
+        * SVG `foreignObject` element). It is not handled as foreign content; therefore, it
+        * closes the open HTML `TEMPLATE` element (the first `<template>` token) - _not_ the
+        * SVG `TEMPLATE` element (the second `<template>` token).
+        *
+        * The test is included here because it may show up as unsupported markup and be skipped by
+        * the html5lib test suite.
+        *
+        * @ticket 61576
+        */
+       public function test_template_tag_closes_html_template_element() {
+               $processor = WP_HTML_Processor::create_fragment( '<template><svg><template><foreignObject><div></template><div>' );
+
+               $this->assertTrue( $processor->next_tag( 'DIV' ) );
+               $this->assertSame( array( 'HTML', 'BODY', 'TEMPLATE', 'SVG', 'TEMPLATE', 'FOREIGNOBJECT', 'DIV' ), $processor->get_breadcrumbs() );
+               $this->assertTrue( $processor->next_tag( 'DIV' ) );
+               $this->assertSame( array( 'HTML', 'BODY', 'DIV' ), $processor->get_breadcrumbs() );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Ensures that the tag processor is case sensitive when removing CSS classes in no-quirks mode.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 61531
</span></span></pre>
</div>
</div>

</body>
</html>