Description of the bug
When a document is parsed with the libxml HTML parser (qp() on a .html file, or htmlqp()), any processing instruction in it — most commonly a <?php … ?> block — gains an extra ? every time the document is serialized with html(), innerHTML(), innerXML(), html5(), or innerHTML5().
<?php echo $title; ?> -> <?php echo $title; ??>
Round-trip the document twice and you get ???>, and so on. The output is no longer valid PHP.
writeHTML() and writeHTML5() are not affected, and neither is a document parsed by html5qp() or qp() in XML mode.
Root cause
libxml's HTML parser stores the closing ? of a PI as part of the node's data, while its XML parser does not:
$d = new DOMDocument();
$d->loadHTML('<html><body><h1><?php echo $t; ?></h1></body></html>');
// data === 'echo $t; ?' <-- trailing "?" retained
$d2 = new DOMDocument();
$d2->loadXML('<root><?php echo $t; ?></root>');
// data === 'echo $t; ' <-- no trailing "?"
saveHTML() compensates for this and emits a single ?. saveXML() does not — it appends ?> to data that already ends in ?:
$d->saveHTML($body); // <body><h1><?php echo $t; ?></h1></body> correct
$d->saveXML($body); // <body><h1><?php echo $t; ??></h1></body> corrupted
QueryPath hits this because DOMQuery::html() serializes a non-root node with saveXML() (src/DOMQuery.php:707), and the html5() family hands the same node to the Masterminds serializer, which likewise does not compensate.
Suggested fix
In the HTML-oriented serializers, either use saveHTML($node) rather than saveXML($node) for a document that was HTML-parsed, or normalise the PI data on load by stripping a single trailing ?. The second option also fixes the reading side — $pi->data currently returns PHP source with a stray ? glued to the end, which callers have to trim themselves.
QueryPath version
4.1.0 (main, 3036f97)
PHP Version and environment
PHP 8.3.16 (cli), libxml 2.9.13, macOS. Not version-specific — it reproduces anywhere libxml behaves this way.
Minimal reproducible PHP+HTML snippet to replicate bug
<?php
require 'vendor/autoload.php';
$html = '<html><body><h1><?php echo $title; ?></h1></body></html>';
$qp = htmlqp($html);
echo $qp->top()->find('body')->innerHTML(), "\n";
// actual: <h1><?php echo $title; ??></h1>
// expected: <h1><?php echo $title; ?></h1>
$qp->top()->writeHTML();
// writeHTML() is correct:
// <html><body><h1><?php echo $title; ?></h1></body></html>
Description of the bug
When a document is parsed with the libxml HTML parser (
qp()on a.htmlfile, orhtmlqp()), any processing instruction in it — most commonly a<?php … ?>block — gains an extra?every time the document is serialized withhtml(),innerHTML(),innerXML(),html5(), orinnerHTML5().Round-trip the document twice and you get
???>, and so on. The output is no longer valid PHP.writeHTML()andwriteHTML5()are not affected, and neither is a document parsed byhtml5qp()orqp()in XML mode.Root cause
libxml's HTML parser stores the closing
?of a PI as part of the node'sdata, while its XML parser does not:saveHTML()compensates for this and emits a single?.saveXML()does not — it appends?>to data that already ends in?:QueryPath hits this because
DOMQuery::html()serializes a non-root node withsaveXML()(src/DOMQuery.php:707), and thehtml5()family hands the same node to the Masterminds serializer, which likewise does not compensate.Suggested fix
In the HTML-oriented serializers, either use
saveHTML($node)rather thansaveXML($node)for a document that was HTML-parsed, or normalise the PI data on load by stripping a single trailing?. The second option also fixes the reading side —$pi->datacurrently returns PHP source with a stray?glued to the end, which callers have to trim themselves.QueryPath version
4.1.0 (
main, 3036f97)PHP Version and environment
PHP 8.3.16 (cli), libxml 2.9.13, macOS. Not version-specific — it reproduces anywhere libxml behaves this way.
Minimal reproducible PHP+HTML snippet to replicate bug