[php-src] Issue #21876: XMLReader::readOuterXml() returns empty string for DOCTYPE nodes since libxml2 2.13.0

[email protected] (lahwaacz)
Newsgroups php.bugs
Message-ID <[email protected]>
Issue: https://github.com/php/php-src/issues/21876
Author: lahwaacz

### Description

The following code:

```php
<?php
$svg = <<<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300">
  <rect width="400" height="300" fill="white"/>
</svg>
XML;

$reader = new XMLReader();
$reader->XML($svg, null, LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_NONET);
$reader->setParserProperty(XMLReader::SUBST_ENTITIES, true);

while (@$reader->read()) {
    if ($reader->nodeType == XMLReader::DOC_TYPE) {
        $outer = $reader->readOuterXml();
        $inner = $reader->readInnerXml();
        echo "  readOuterXml(): " . ($outer !== '' ? '"' . substr($outer, 0, 120) . '"' : '[EMPTY STRING]') . "\n";
        echo "  readOuterXml() length: " . strlen($outer) . "\n";
        echo "  readInnerXml(): " . ($inner !== '' ? '"' . substr($inner, 0, 120) . '"' : '[EMPTY STRING]') . "\n";
        echo "  readInnerXml() length: " . strlen($inner) . "\n";
    }
}
$reader->close();
```

Results in this output with old libxml2 (<= 2.12.9):

```
readOuterXml(): "<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">"
readOuterXml() length: 98
readInnerXml(): [EMPTY STRING]
readInnerXml() length: 0
```

But the behavior is different with libxml2 >= 2.13.0:
```
  readOuterXml(): [EMPTY STRING]
  readOuterXml() length: 0
  readInnerXml(): [EMPTY STRING]
  readInnerXml() length: 0
```

## Root cause

libxml2 commit [reader: Rework xmlTextReaderRead{Inner,Outer}Xml](https://gitlab.gnome.org/GNOME/libxml2/-/commit/f69647811c6c86c42bd9cb036fc76db5ce5fc6b0) (2024-04-22, released in v2.13.0) introduced `xmlTextReaderDumpCopy()` which serializes reader nodes into a buffer. This function skips `XML_DTD_NODE` entries, whereas the old code had explicit `xmlCopyDtd()` handling that correctly serialized DTD nodes.

Is this is an upstream libxml2 regression or intended behavior? Does PHP need a workaround for XMLReader?

This issue affects at least MediaWiki (Uploading SVG file generated by matplotlib fails with libxml2 >= 2.13.0): https://phabricator.wikimedia.org/T399990

## Reproducer

Two self-contained scripts that build libxml2 from source at specific versions inside a Fedora 41 Docker container:

- `host_php.sh` — host-side driver (usage: `./host_php.sh [good|bad|both]`)
- `container_php.sh` — runs inside the container, builds libxml2, runs the PHP test

Usage:
```bash
./host_php.sh both
```

[container_php.sh](https://github.com/user-attachments/files/27101284/container_php.sh)
[host_php.sh](https://github.com/user-attachments/files/27101285/host_php.sh)

### PHP Version

```plain
PHP 8.3.x / 8.4.x (any version linked against libxml2 >= 2.13.0)
```

### Operating System

Arch Linux (rolling), also confirmed on Alpine edge, Debian sid, and inside Fedora 41 containers with custom-built libxml2.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.